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>
73 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|