feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
"""Tests for directdnsonly.app.api.admin — DNSAdminAPI handler methods."""
|
2026-02-18 22:53:09 +13:00
|
|
|
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
import pytest
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
from urllib.parse import parse_qs
|
|
|
|
|
|
|
|
|
|
import cherrypy
|
|
|
|
|
|
|
|
|
|
from directdnsonly.app.api.admin import DNSAdminAPI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fixtures
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def save_queue():
|
|
|
|
|
return MagicMock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def delete_queue():
|
|
|
|
|
return MagicMock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def api(save_queue, delete_queue):
|
|
|
|
|
return DNSAdminAPI(save_queue, delete_queue, backend_registry=MagicMock())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# CMD_API_LOGIN_TEST
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_login_test_returns_success(api):
|
|
|
|
|
result = api.CMD_API_LOGIN_TEST()
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["0"]
|
|
|
|
|
assert parsed["text"] == ["Login OK"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# _handle_exists — GET action=exists
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_handle_exists_missing_domain_returns_error(api):
|
|
|
|
|
with patch.object(cherrypy, "response", MagicMock()):
|
|
|
|
|
result = api._handle_exists({"action": "exists"})
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["1"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_handle_exists_unsupported_action_returns_error(api):
|
|
|
|
|
with patch.object(cherrypy, "response", MagicMock()):
|
|
|
|
|
result = api._handle_exists({"action": "rawsave"})
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["1"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_handle_exists_domain_not_found(api):
|
2026-02-18 22:53:09 +13:00
|
|
|
with (
|
|
|
|
|
patch("directdnsonly.app.api.admin.check_zone_exists", return_value=False),
|
|
|
|
|
patch(
|
|
|
|
|
"directdnsonly.app.api.admin.check_parent_domain_owner", return_value=False
|
|
|
|
|
),
|
|
|
|
|
):
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
result = api._handle_exists({"action": "exists", "domain": "example.com"})
|
|
|
|
|
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["0"]
|
|
|
|
|
assert parsed["exists"] == ["0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_handle_exists_domain_found(api):
|
|
|
|
|
record = MagicMock()
|
|
|
|
|
record.hostname = "da1.example.com"
|
|
|
|
|
|
2026-02-18 22:53:09 +13:00
|
|
|
with (
|
|
|
|
|
patch("directdnsonly.app.api.admin.check_zone_exists", return_value=True),
|
|
|
|
|
patch("directdnsonly.app.api.admin.get_domain_record", return_value=record),
|
|
|
|
|
):
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
result = api._handle_exists({"action": "exists", "domain": "example.com"})
|
|
|
|
|
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["0"]
|
|
|
|
|
assert parsed["exists"] == ["1"]
|
|
|
|
|
assert "da1.example.com" in parsed["details"][0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_handle_exists_parent_found(api):
|
|
|
|
|
parent = MagicMock()
|
|
|
|
|
parent.hostname = "da2.example.com"
|
|
|
|
|
|
2026-02-18 22:53:09 +13:00
|
|
|
with (
|
|
|
|
|
patch("directdnsonly.app.api.admin.check_zone_exists", return_value=False),
|
|
|
|
|
patch(
|
|
|
|
|
"directdnsonly.app.api.admin.check_parent_domain_owner", return_value=True
|
|
|
|
|
),
|
|
|
|
|
patch(
|
|
|
|
|
"directdnsonly.app.api.admin.get_parent_domain_record", return_value=parent
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
result = api._handle_exists(
|
|
|
|
|
{
|
|
|
|
|
"action": "exists",
|
|
|
|
|
"domain": "sub.example.com",
|
|
|
|
|
"check_for_parent_domain": "1",
|
|
|
|
|
}
|
|
|
|
|
)
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["0"]
|
|
|
|
|
assert parsed["exists"] == ["2"]
|
|
|
|
|
assert "da2.example.com" in parsed["details"][0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_handle_exists_no_parent_check_when_flag_absent(api):
|
|
|
|
|
"""check_parent_domain_owner should not be called if flag not set."""
|
|
|
|
|
record = MagicMock()
|
|
|
|
|
record.hostname = "da1.example.com"
|
|
|
|
|
|
2026-02-18 22:53:09 +13:00
|
|
|
with (
|
|
|
|
|
patch("directdnsonly.app.api.admin.check_zone_exists", return_value=True),
|
|
|
|
|
patch("directdnsonly.app.api.admin.check_parent_domain_owner") as mock_parent,
|
|
|
|
|
patch("directdnsonly.app.api.admin.get_domain_record", return_value=record),
|
|
|
|
|
):
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
api._handle_exists({"action": "exists", "domain": "example.com"})
|
|
|
|
|
|
|
|
|
|
mock_parent.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# _handle_rawsave
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
SAMPLE_ZONE = "$ORIGIN example.com.\n$TTL 300\nexample.com. 300 IN A 1.2.3.4\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rawsave_enqueues_item(api, save_queue):
|
2026-02-18 22:53:09 +13:00
|
|
|
with (
|
|
|
|
|
patch(
|
|
|
|
|
"directdnsonly.app.api.admin.validate_and_normalize_zone",
|
|
|
|
|
return_value=SAMPLE_ZONE,
|
|
|
|
|
),
|
|
|
|
|
patch.object(cherrypy, "request", MagicMock(remote=MagicMock(ip="127.0.0.1"))),
|
|
|
|
|
):
|
|
|
|
|
result = api._handle_rawsave(
|
|
|
|
|
"example.com",
|
|
|
|
|
{
|
|
|
|
|
"zone_file": SAMPLE_ZONE,
|
|
|
|
|
"hostname": "da1.example.com",
|
|
|
|
|
"username": "admin",
|
|
|
|
|
},
|
|
|
|
|
)
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
|
|
|
|
|
save_queue.put.assert_called_once()
|
|
|
|
|
item = save_queue.put.call_args[0][0]
|
|
|
|
|
assert item["domain"] == "example.com"
|
|
|
|
|
assert item["hostname"] == "da1.example.com"
|
|
|
|
|
assert item["username"] == "admin"
|
|
|
|
|
assert item["client_ip"] == "127.0.0.1"
|
|
|
|
|
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rawsave_missing_zone_file_raises(api):
|
|
|
|
|
with patch.object(cherrypy, "request", MagicMock(remote=MagicMock(ip="127.0.0.1"))):
|
|
|
|
|
with pytest.raises(ValueError, match="Missing zone file"):
|
|
|
|
|
api._handle_rawsave("example.com", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rawsave_invalid_zone_raises(api):
|
2026-02-18 22:53:09 +13:00
|
|
|
with (
|
|
|
|
|
patch(
|
|
|
|
|
"directdnsonly.app.api.admin.validate_and_normalize_zone",
|
|
|
|
|
side_effect=ValueError("Invalid zone data: bad record"),
|
|
|
|
|
),
|
|
|
|
|
patch.object(cherrypy, "request", MagicMock(remote=MagicMock(ip="127.0.0.1"))),
|
|
|
|
|
):
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
with pytest.raises(ValueError, match="Invalid zone data"):
|
|
|
|
|
api._handle_rawsave("example.com", {"zone_file": "garbage"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# _handle_delete
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_enqueues_item(api, delete_queue):
|
|
|
|
|
with patch.object(cherrypy, "request", MagicMock(remote=MagicMock(ip="10.0.0.1"))):
|
2026-02-18 22:53:09 +13:00
|
|
|
result = api._handle_delete(
|
|
|
|
|
"example.com",
|
|
|
|
|
{
|
|
|
|
|
"hostname": "da1.example.com",
|
|
|
|
|
"username": "admin",
|
|
|
|
|
},
|
|
|
|
|
)
|
feat: add test suite, fix backend bugs, remove legacy artifacts 🧪
- Add 73-test suite across conftest, utils, admin API, reconciler, zone parser,
and CoreDNS MySQL backend (all green, ~0.5s)
- Fix zone_exists filter using wrong column name (name → zone_name)
- Fix delete_zone missing dot_fqdn normalization on lookup
- Remove spurious unused `from config import config` in coredns_mysql.py
- Fix config loader to search module-relative path so tests find app.yml
without needing a root-level config/ directory
- Remove legacy v1 Flask prototype (app.py), empty config.json, and
duplicate root config/app.yml
2026-02-18 22:03:04 +13:00
|
|
|
|
|
|
|
|
delete_queue.put.assert_called_once()
|
|
|
|
|
item = delete_queue.put.call_args[0][0]
|
|
|
|
|
assert item["domain"] == "example.com"
|
|
|
|
|
assert item["hostname"] == "da1.example.com"
|
|
|
|
|
assert item["client_ip"] == "10.0.0.1"
|
|
|
|
|
|
|
|
|
|
parsed = parse_qs(result)
|
|
|
|
|
assert parsed["error"] == ["0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_missing_params_uses_empty_strings(api, delete_queue):
|
|
|
|
|
with patch.object(cherrypy, "request", MagicMock(remote=MagicMock(ip="127.0.0.1"))):
|
|
|
|
|
api._handle_delete("example.com", {})
|
|
|
|
|
|
|
|
|
|
item = delete_queue.put.call_args[0][0]
|
|
|
|
|
assert item["hostname"] == ""
|
|
|
|
|
assert item["username"] == ""
|