technitium 0.4.0

Typed async Rust client for the Technitium DNS Server API
Documentation
//! Serde round-trip contract tests.
//!
//! These tests verify that our types correctly deserialize sample JSON
//! fixtures captured from real Technitium API responses. They run offline
//! (no server needed) and catch type-mapping regressions early.

use technitium::*;

fn load_fixture(name: &str) -> serde_json::Value {
    let path = format!("tests/fixtures/{name}");
    let data = std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("failed to read fixture {path}: {e}"));
    serde_json::from_str(&data).unwrap_or_else(|e| panic!("failed to parse fixture {path}: {e}"))
}

#[test]
fn zone_list_deserializes() {
    let json = load_fixture("zone_list.json");
    let zones_value = json.get("zones").expect("missing zones key");
    let zones: Vec<Zone> =
        serde_json::from_value(zones_value.clone()).expect("failed to deserialize zones");
    assert_eq!(zones.len(), 2);
    assert_eq!(zones[0].name, "example.com");
    assert_eq!(zones[0].zone_type, ZoneType::Primary);
    assert!(!zones[0].disabled);
    assert_eq!(zones[1].name, "test.local");
    assert_eq!(zones[1].zone_type, ZoneType::Forwarder);
    assert!(zones[1].disabled);
}

#[test]
fn record_list_deserializes() {
    let json = load_fixture("record_list.json");
    let records_value = json.get("records").expect("missing records key");
    let records: Vec<Record> =
        serde_json::from_value(records_value.clone()).expect("failed to deserialize records");
    assert_eq!(records.len(), 3);

    assert_eq!(records[0].name, "example.com");
    assert!(matches!(records[0].record_type, RecordType::SOA));
    assert_eq!(records[0].ttl, technitium::Ttl(3600));

    assert_eq!(records[1].name, "app.example.com");
    assert!(matches!(records[1].record_type, RecordType::A));
    assert_eq!(records[1].data["ipAddress"], "10.0.0.1");

    assert_eq!(records[2].name, "example.com");
    assert!(matches!(records[2].record_type, RecordType::MX));
    assert_eq!(records[2].data["preference"], 10);
    assert_eq!(records[2].data["exchange"], "mail.example.com");
}

#[test]
fn dns_resolve_deserializes() {
    let json = load_fixture("dns_resolve.json");

    // Extract the flattened response (status + answer/authority/additional)
    let result: DnsQueryResult =
        serde_json::from_value(json.clone()).expect("failed to deserialize DnsQueryResult");
    assert_eq!(result.answer.len(), 1);
    assert_eq!(result.answer[0].name, "example.com");
    assert_eq!(result.answer[0].record_type, RecordType::A);
    assert!(result.authority.is_empty());
    assert!(result.additional.is_empty());
}

#[test]
fn dashboard_stats_deserializes() {
    let json = load_fixture("dashboard_stats.json");
    let stats: DashboardStats =
        serde_json::from_value(json.clone()).expect("failed to deserialize DashboardStats");
    let s = stats.stats.expect("stats should be present");
    assert_eq!(s.total_queries, 150);
    assert_eq!(s.total_no_error, 140);
    assert_eq!(s.total_blocked, 10);
}

#[test]
fn server_settings_deserializes() {
    let json = load_fixture("settings.json");
    let settings: ServerSettings =
        serde_json::from_value(json.clone()).expect("failed to deserialize ServerSettings");
    assert_eq!(settings.dns_server_domain.as_deref(), Some("dns.local"));
    assert_eq!(
        settings.forwarders.as_deref(),
        Some(vec!["1.1.1.1".to_string(), "8.8.8.8".to_string()].as_slice())
    );
    assert_eq!(settings.enable_recursion, Some(true));
    assert_eq!(settings.dnssec_validation, Some(true));
}