// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "database/sql" "fmt" "os" "testing" "time" _ "github.com/go-sql-driver/mysql" ) // Isolated test schema, per PLAN.md Phase 1: a dedicated table // (test_pdnsapi_records) on the live MariaDB, never the real coredns_records // table. TABLE_PREFIX for the Store under test is "test_pdnsapi_". const testTablePrefix = "test_pdnsapi_" const testTableName = testTablePrefix + "records" var ( testDB *sql.DB testDBReason string // non-empty if the DB is unreachable; tests should t.Skip with this testDBReady bool ) func TestMain(m *testing.M) { dsn := os.Getenv("MYSQL_DSN") if dsn == "" { dsn = "coredns:coredns@tcp(127.0.0.1:3306)/coredns?parseTime=true" } db, err := sql.Open("mysql", dsn) if err != nil { testDBReason = fmt.Sprintf("mysql: sql.Open failed: %v", err) } else { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) pingErr := db.PingContext(ctx) cancel() if pingErr != nil { testDBReason = fmt.Sprintf("mysql: unreachable at %s: %v", dsn, pingErr) } else { testDB = db testDBReady = true } } code := m.Run() if testDB != nil { // Final safety net teardown in case an individual test's defer was // skipped (e.g. t.Fatal before cleanup registration). Never touches // coredns_records — only the isolated test_pdnsapi_records table. _, _ = testDB.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", testTableName)) testDB.Close() } os.Exit(code) } // requireDB skips the calling test if the live MariaDB is unreachable. func requireDB(t *testing.T) *sql.DB { t.Helper() if !testDBReady { t.Skipf("skipping: live MySQL/MariaDB not reachable (%s)", testDBReason) } return testDB } // createTestSchema (re)creates test_pdnsapi_records with the exact live // schema from docs/SEMANTICS.md §1, and registers a cleanup to drop it. func createTestSchema(t *testing.T) { t.Helper() db := requireDB(t) ctx := context.Background() _, err := db.ExecContext(ctx, fmt.Sprintf(`DROP TABLE IF EXISTS %s`, testTableName)) if err != nil { t.Fatalf("drop existing test table: %v", err) } _, err = db.ExecContext(ctx, fmt.Sprintf(`CREATE TABLE %s ( id int(11) NOT NULL AUTO_INCREMENT, zone varchar(255) NOT NULL, name varchar(255) NOT NULL, ttl int(11) DEFAULT NULL, content text DEFAULT NULL, record_type varchar(255) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`, testTableName)) if err != nil { t.Fatalf("create test table: %v", err) } t.Cleanup(func() { _, err := db.ExecContext(context.Background(), fmt.Sprintf(`DROP TABLE IF EXISTS %s`, testTableName)) if err != nil { t.Errorf("drop test table on cleanup: %v", err) } }) } // seedRow is one row to insert during reseed. type seedRow struct { zone string name string ttl sql.NullInt64 content string recordType string } // exampleComSeed is the SOA/NS/A/CNAME example.com. seed from // docs/SEMANTICS.md §1 (mirrors the live coredns_records example.com data), // plus two TXT rows on one name to exercise rrset aggregation. func exampleComSeed() []seedRow { nn := func(v int64) sql.NullInt64 { return sql.NullInt64{Int64: v, Valid: true} } return []seedRow{ {"example.com.", "", nn(300), `{"ttl":300, "mbox":"hostmaster.example.com.", "ns":"ns1.example.com.", "refresh":44, "retry":55, "expire":66}`, "SOA"}, {"example.com.", "", nn(300), `{"host":"ns1.example.com."}`, "NS"}, {"example.com.", "", nn(300), `{"ip":"1.2.3.4"}`, "A"}, {"example.com.", "www", nn(300), `{"host":"example.com."}`, "CNAME"}, {"example.com.", "multi", nn(120), `{"text":"first"}`, "TXT"}, {"example.com.", "multi", nn(120), `{"text":"second"}`, "TXT"}, } } // reseed truncates test_pdnsapi_records and inserts the known seed rows. // Call at the start of every DB-backed test that reads or mutates rows, since // the table is shared mutable state across tests in the same run. func reseed(t *testing.T, db *sql.DB, rows []seedRow) { t.Helper() ctx := context.Background() if _, err := db.ExecContext(ctx, fmt.Sprintf("TRUNCATE TABLE %s", testTableName)); err != nil { t.Fatalf("truncate test table: %v", err) } insert := fmt.Sprintf("INSERT INTO %s (zone, name, ttl, content, record_type) VALUES (?, ?, ?, ?, ?)", testTableName) for _, row := range rows { if _, err := db.ExecContext(ctx, insert, row.zone, row.name, row.ttl, row.content, row.recordType); err != nil { t.Fatalf("seed insert %+v: %v", row, err) } } } // --- Store-level tests ------------------------------------------------- func TestStoreListZones(t *testing.T) { createTestSchema(t) db := requireDB(t) reseed(t, db, exampleComSeed()) st := NewStore(db, testTablePrefix) zones, err := st.ListZones(context.Background()) if err != nil { t.Fatal(err) } if len(zones) != 1 || zones[0] != "example.com." { t.Fatalf("zones = %v, want [example.com.]", zones) } } func TestStoreZoneExists(t *testing.T) { createTestSchema(t) db := requireDB(t) reseed(t, db, exampleComSeed()) st := NewStore(db, testTablePrefix) ok, err := st.ZoneExists(context.Background(), "example.com.") if err != nil { t.Fatal(err) } if !ok { t.Fatal("expected example.com. to exist") } ok, err = st.ZoneExists(context.Background(), "nope.com.") if err != nil { t.Fatal(err) } if ok { t.Fatal("expected nope.com. to not exist") } } func TestStoreGetZoneRecords(t *testing.T) { createTestSchema(t) db := requireDB(t) reseed(t, db, exampleComSeed()) st := NewStore(db, testTablePrefix) records, err := st.GetZoneRecords(context.Background(), "example.com.") if err != nil { t.Fatal(err) } if len(records) != len(exampleComSeed()) { t.Fatalf("got %d records, want %d", len(records), len(exampleComSeed())) } } func TestStoreApplyRRsetsReplace(t *testing.T) { createTestSchema(t) db := requireDB(t) reseed(t, db, exampleComSeed()) st := NewStore(db, testTablePrefix) changes := []RRsetChange{ { Zone: "example.com.", Name: "www", RecordType: "CNAME", ChangeType: "REPLACE", TTL: 60, Contents: []string{`{"host":"other.example.com."}`}, }, } if err := st.ApplyRRsets(context.Background(), changes); err != nil { t.Fatal(err) } rows, err := db.QueryContext(context.Background(), fmt.Sprintf("SELECT content FROM %s WHERE zone=? AND name=? AND record_type=?", testTableName), "example.com.", "www", "CNAME") if err != nil { t.Fatal(err) } defer rows.Close() var contents []string for rows.Next() { var c string if err := rows.Scan(&c); err != nil { t.Fatal(err) } contents = append(contents, c) } if err := rows.Err(); err != nil { t.Fatal(err) } if len(contents) != 1 || contents[0] != `{"host":"other.example.com."}` { t.Fatalf("contents = %v, want single other.example.com. row", contents) } } func TestStoreApplyRRsetsDelete(t *testing.T) { createTestSchema(t) db := requireDB(t) reseed(t, db, exampleComSeed()) st := NewStore(db, testTablePrefix) changes := []RRsetChange{ {Zone: "example.com.", Name: "www", RecordType: "CNAME", ChangeType: "DELETE"}, } if err := st.ApplyRRsets(context.Background(), changes); err != nil { t.Fatal(err) } var count int err := db.QueryRowContext(context.Background(), fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE zone=? AND name=? AND record_type=?", testTableName), "example.com.", "www", "CNAME").Scan(&count) if err != nil { t.Fatal(err) } if count != 0 { t.Fatalf("count = %d, want 0 after DELETE", count) } }