Files
jarl/internal/porkbun/porkbun_test.go
abrown38andClaude Opus 5.5 5630e7a848 Add DNSSEC signing, key backups and Porkbun support
Zones get a DNSSEC setting: named signs them with BIND's default (or a
custom) dnssec-policy and inline signing, with keys in a per-view key
directory. Jarl shows the keys, the DS record and when it is safe to
publish it, checks the parent's DS records (check-ds, which also tells
named), and refuses steps that would break a signed domain: switching
straight to unsigned, finishing a turn-off too early, deleting or moving
a signed zone, TTLs above max-zone-ttl. Turning off goes through the
insecure policy; wipe starts over once the parent has no DS.

Backups store the DNSSEC keys and restore puts them back for named; the
backup unit gets CAP_DAC_READ_SEARCH to read them. Jarl raises a zone's
serial above named's whenever it writes an older one (after a restore).
The serials check compares the signed serial.

With a Porkbun API key in jarl.yaml, the registrar's name servers, glue
records and DS records can be shown and changed, each change checked
first (dry run, force to override).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:19:04 +00:00

73 lines
2.6 KiB
Go

package porkbun
import (
"context"
"errors"
"net/http/httptest"
"strings"
"testing"
)
func TestClient(t *testing.T) {
f := NewFake("pk1_x", "sk1_y", "adbraun.lol")
ts := httptest.NewServer(f)
defer ts.Close()
c := &Client{BaseURL: ts.URL + "/api/json/v3", APIKey: "pk1_x", Secret: "sk1_y"}
ctx := context.Background()
if acct, _, err := c.Ping(ctx); err != nil || acct != "tester" {
t.Fatalf("ping: %q %v", acct, err)
}
ns, err := c.NS(ctx, "adbraun.lol.")
if err != nil || strings.Join(ns, " ") != "curitiba.ns.porkbun.com fortaleza.ns.porkbun.com" {
t.Errorf("NS: %v %v", ns, err)
}
// Two glue records, one with IPv4 and IPv6.
if err := c.SetGlue(ctx, "adbraun.lol", "ns1.adbraun.lol.", []string{"203.0.113.1", "2001:db8::1"}, false); err != nil {
t.Fatal(err)
}
if err := c.SetGlue(ctx, "adbraun.lol", "ns2.adbraun.lol", []string{"203.0.113.2"}, false); err != nil {
t.Fatal(err)
}
if err := c.SetGlue(ctx, "adbraun.lol", "ns2.adbraun.lol", []string{"203.0.113.3"}, true); err != nil {
t.Fatal(err)
}
if err := c.SetGlue(ctx, "adbraun.lol", "ns.elsewhere.net", nil, false); err == nil {
t.Error("glue outside the domain accepted")
}
glue, err := c.Glue(ctx, "adbraun.lol")
if err != nil || len(glue) != 2 || glue[0].Host != "ns1.adbraun.lol" || strings.Join(glue[0].Addresses(), " ") != "203.0.113.1 2001:db8::1" || glue[1].V4[0] != "203.0.113.3" {
t.Errorf("glue: %+v %v", glue, err)
}
if err := c.SetNS(ctx, "adbraun.lol", []string{"ns1.adbraun.lol.", "ns2.adbraun.lol"}); err != nil {
t.Fatal(err)
}
if err := c.DeleteGlue(ctx, "adbraun.lol", "ns2.adbraun.lol"); err == nil {
t.Error("deleted glue in use")
}
// DS records: empty comes as [], others keyed by tag.
if ds, err := c.DS(ctx, "adbraun.lol"); err != nil || len(ds) != 0 {
t.Errorf("no DS: %v %v", ds, err)
}
if _, err := c.AddDS(ctx, "adbraun.lol", DS{KeyTag: 22642, Algorithm: 13, DigestType: 2, Digest: "ABCDEF"}); err != nil {
t.Fatal(err)
}
if ds, err := c.DS(ctx, "adbraun.lol"); err != nil || len(ds) != 1 || ds[0].KeyTag != 22642 || ds[0].Algorithm != 13 || ds[0].Digest != "ABCDEF" {
t.Errorf("DS: %+v %v", ds, err)
}
if err := c.DeleteDS(ctx, "adbraun.lol", 22642); err != nil {
t.Fatal(err)
}
// Errors carry Porkbun's code.
f.Domains["adbraun.lol"].APIAccess = false
_, err = c.NS(ctx, "adbraun.lol")
var pe *Error
if !errors.As(err, &pe) || pe.Code != "API_ACCESS_DISABLED" {
t.Errorf("error: %v", err)
}
bad := &Client{BaseURL: c.BaseURL, APIKey: "pk1_x", Secret: "wrong"}
if _, _, err := bad.Ping(ctx); err == nil || !strings.Contains(err.Error(), "INVALID_API_KEYS") {
t.Errorf("bad key: %v", err)
}
}