first commit

This commit is contained in:
2026-07-09 22:16:19 +02:00
commit 869749bb01
34 changed files with 3204 additions and 0 deletions

269
store_test.go Normal file
View File

@@ -0,0 +1,269 @@
// 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)
}
}