"""Tests for GET /api/ddns/check/ and PUT /api/ddns.""" import json import sys import os import unittest from unittest.mock import patch, MagicMock sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'api')) def _make_client(): from app import app app.config['TESTING'] = True return app.test_client() class TestDdnsCheckName(unittest.TestCase): def setUp(self): self.client = _make_client() def _get(self, name): return self.client.get(f'/api/ddns/check/{name}') @patch('routes.config.DDNS_API_BASE', 'http://ddns.test', create=True) def test_available_name_returns_true(self): with patch('routes.config._ureq', create=True): import io resp_mock = MagicMock() resp_mock.read.return_value = b'{"available": true}' resp_mock.__enter__ = lambda s: resp_mock resp_mock.__exit__ = MagicMock(return_value=False) with patch('urllib.request.urlopen', return_value=resp_mock): r = self._get('testname') self.assertEqual(r.status_code, 200) body = json.loads(r.data) self.assertTrue(body['available']) @patch('routes.config.DDNS_API_BASE', 'http://ddns.test', create=True) def test_taken_name_returns_false(self): resp_mock = MagicMock() resp_mock.read.return_value = b'{"available": false}' resp_mock.__enter__ = lambda s: resp_mock resp_mock.__exit__ = MagicMock(return_value=False) with patch('urllib.request.urlopen', return_value=resp_mock): r = self._get('taken') self.assertEqual(r.status_code, 200) body = json.loads(r.data) self.assertFalse(body['available']) def test_unreachable_returns_503(self): import urllib.error with patch('urllib.request.urlopen', side_effect=OSError('conn refused')): r = self._get('anything') self.assertEqual(r.status_code, 503) body = json.loads(r.data) self.assertIsNone(body['available']) class TestUpdateDdnsConfig(unittest.TestCase): def setUp(self): self.client = _make_client() def _put(self, payload): return self.client.put( '/api/ddns', data=json.dumps(payload), content_type='application/json', ) def test_invalid_domain_mode_returns_400(self): r = self._put({'domain_mode': 'invalid_mode'}) self.assertEqual(r.status_code, 400) self.assertIn('domain_mode', json.loads(r.data)['error']) def test_cloudflare_requires_domain_name(self): r = self._put({'domain_mode': 'cloudflare', 'cloudflare_api_token': 'tok'}) self.assertEqual(r.status_code, 400) self.assertIn('domain_name', json.loads(r.data)['error']) def test_cloudflare_invalid_token_returns_422(self): import urllib.error with patch('urllib.request.urlopen', side_effect=urllib.error.HTTPError( None, 403, 'Forbidden', {}, None )): r = self._put({'domain_mode': 'cloudflare', 'domain_name': 'home.example.com', 'cloudflare_api_token': 'bad-token'}) self.assertEqual(r.status_code, 422) def test_cloudflare_valid_token_saves_config(self): from app import config_manager resp_mock = MagicMock() resp_mock.read.return_value = b'{"success": true}' resp_mock.__enter__ = lambda s: resp_mock resp_mock.__exit__ = MagicMock(return_value=False) with patch('urllib.request.urlopen', return_value=resp_mock): with patch.object(config_manager, 'set_ddns_config') as mock_set_ddns, \ patch.object(config_manager, 'set_identity_field') as mock_set_id: r = self._put({'domain_mode': 'cloudflare', 'domain_name': 'home.example.com', 'cloudflare_api_token': 'valid-token'}) self.assertEqual(r.status_code, 200) self.assertTrue(json.loads(r.data)['updated']) mock_set_ddns.assert_called_once() mock_set_id.assert_any_call('domain_mode', 'cloudflare') def test_duckdns_requires_domain_name(self): r = self._put({'domain_mode': 'duckdns', 'duckdns_token': 'tok'}) self.assertEqual(r.status_code, 400) def test_duckdns_invalid_token_returns_422(self): resp_mock = MagicMock() resp_mock.read.return_value = b'KO' resp_mock.__enter__ = lambda s: resp_mock resp_mock.__exit__ = MagicMock(return_value=False) with patch('urllib.request.urlopen', return_value=resp_mock): r = self._put({'domain_mode': 'duckdns', 'domain_name': 'myname.duckdns.org', 'duckdns_token': 'bad'}) self.assertEqual(r.status_code, 422) def test_lan_mode_saves_without_validation(self): from app import config_manager with patch.object(config_manager, 'set_ddns_config') as mock_ddns, \ patch.object(config_manager, 'set_identity_field') as mock_id: r = self._put({'domain_mode': 'lan'}) self.assertEqual(r.status_code, 200) mock_ddns.assert_called_once() mock_id.assert_any_call('domain_mode', 'lan') def test_http01_mode_saves_with_domain(self): from app import config_manager with patch.object(config_manager, 'set_ddns_config') as mock_ddns, \ patch.object(config_manager, 'set_identity_field') as mock_id: r = self._put({'domain_mode': 'http01', 'domain_name': 'home.example.com'}) self.assertEqual(r.status_code, 200) mock_id.assert_any_call('domain_name', 'home.example.com') class TestDdnsRegister(unittest.TestCase): def setUp(self): self.client = _make_client() def test_non_pic_ngo_provider_returns_400(self): from app import config_manager with patch.object(config_manager, 'configs', {'ddns': {'provider': 'cloudflare'}, '_identity': {}}): r = self.client.post('/api/ddns/register') self.assertEqual(r.status_code, 400) def test_missing_cell_name_returns_400(self): from app import config_manager with patch.object(config_manager, 'configs', {'ddns': {'provider': 'pic_ngo'}, '_identity': {}}): r = self.client.post('/api/ddns/register') self.assertEqual(r.status_code, 400) self.assertIn('cell_name', json.loads(r.data)['error']) def test_register_success(self): from app import config_manager from ddns_manager import DDNSManager with patch.object(config_manager, 'configs', { 'ddns': {'provider': 'pic_ngo'}, '_identity': {'cell_name': 'mypic'} }): with patch.object(DDNSManager, 'register', return_value={'subdomain': 'mypic.pic.ngo', 'token': 'tok'}) as mock_reg, \ patch.object(config_manager, 'set_identity_field') as mock_id: r = self.client.post('/api/ddns/register') self.assertEqual(r.status_code, 200) body = json.loads(r.data) self.assertTrue(body['registered']) self.assertEqual(body['subdomain'], 'mypic.pic.ngo') if __name__ == '__main__': unittest.main()