You've already forked coredns-powerdns-api-wrapper
270 lines
7.4 KiB
Go
270 lines
7.4 KiB
Go
// 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)
|
|
}
|
|
}
|