feat: production-ready domain control dashboard
Container-Image veröffentlichen / container (push) Has been cancelled
Container-Image veröffentlichen / container (push) Has been cancelled
This commit is contained in:
1 parent
9c60eaf3fe
commit
df7516e7ab
45 files changed
+7932
-1
No files matched your search
@@ -0,0 +1,538 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/hetzner"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/ipresolve"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/model"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/secure"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/store"
|
||||
)
|
||||
|
||||
func TestSyncZoneImportsAddressRecordsAndKeepsArchivedTargetsDetached(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/zones/123/rrsets" || r.Header.Get("Authorization") != "Bearer test-token" {
|
||||
http.Error(w, "unexpected request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"rrsets": [
|
||||
{"name":"@","type":"A","ttl":60,"records":[{"value":"198.51.100.10","comment":"Apex"}]},
|
||||
{"name":"home","type":"A","ttl":120,"records":[{"value":"198.51.100.11","comment":"Router"}]},
|
||||
{"name":"home","type":"AAAA","ttl":120,"records":[{"value":"2001:db8::10","comment":"Router"}]},
|
||||
{"name":"www","type":"CNAME","ttl":300,"records":[{"value":"example.de."}]}
|
||||
],
|
||||
"meta":{"pagination":{"next_page":null}}
|
||||
}`))
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
database, err := store.Open(ctx, filepath.Join(dir, "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
keyFile := filepath.Join(dir, "master-key")
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := secure.LoadCipher(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("test-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "test", ciphertext, nonce, cipher.Fingerprint("test-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "123", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
schedule := New(database, hetzner.New(api.URL), ipresolve.New(), cipher, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
added, err := schedule.SyncZone(ctx, zone.ID)
|
||||
if err != nil || added != 2 {
|
||||
t.Fatalf("expected apex and home to be imported, added=%d err=%v", added, err)
|
||||
}
|
||||
targets, err := database.ListTargets(ctx, zone.ID)
|
||||
if err != nil || len(targets) != 2 {
|
||||
t.Fatalf("expected two address targets, got %#v err=%v", targets, err)
|
||||
}
|
||||
var homeID int64
|
||||
for _, target := range targets {
|
||||
if target.RecordName == "home" {
|
||||
homeID = target.ID
|
||||
if !target.UpdateIPv4 || !target.UpdateIPv6 || target.Active || target.Description != "Router" || target.TTL != 120 {
|
||||
t.Fatalf("home target was not grouped safely: %#v", target)
|
||||
}
|
||||
}
|
||||
}
|
||||
if homeID == 0 {
|
||||
t.Fatal("home target missing")
|
||||
}
|
||||
cnames, err := database.ListCNAMERecords(ctx, zone.ID)
|
||||
if err != nil || len(cnames) != 1 || !cnames[0].Active || cnames[0].RecordName != "www" || cnames[0].Destination != "example.de." || cnames[0].TTL != 300 {
|
||||
t.Fatalf("CNAME was not imported: %#v err=%v", cnames, err)
|
||||
}
|
||||
home, err := database.GetTarget(ctx, homeID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
home.TTL = 60
|
||||
if err := database.UpdateTarget(ctx, home); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added, err = schedule.SyncZone(ctx, zone.ID)
|
||||
if err != nil || added != 0 {
|
||||
t.Fatalf("TTL refresh import failed: added=%d err=%v", added, err)
|
||||
}
|
||||
home, err = database.GetTarget(ctx, homeID)
|
||||
if err != nil || home.TTL != 120 {
|
||||
t.Fatalf("provider TTL was not refreshed on existing target: %#v err=%v", home, err)
|
||||
}
|
||||
if err := database.ArchiveTarget(ctx, homeID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added, err = schedule.SyncZone(ctx, zone.ID)
|
||||
if err != nil || added != 0 {
|
||||
t.Fatalf("re-import should not restore archived target, added=%d err=%v", added, err)
|
||||
}
|
||||
linked, _ := database.ListTargets(ctx, zone.ID)
|
||||
archived, _ := database.ListArchivedTargets(ctx, zone.ID)
|
||||
if len(linked) != 1 || len(archived) != 1 || archived[0].ID != homeID {
|
||||
t.Fatalf("archive state changed during provider import: linked=%#v archived=%#v", linked, archived)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisabledUpdaterRejectsRunsAndTriggerResetsCountdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := store.Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
settings, err := database.GetSettings(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.UpdateSettings(ctx, settings.UpdateIntervalSeconds, false, false, settings.IPv4Endpoints, settings.IPv6Endpoints); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
schedule := New(database, nil, nil, nil, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
if _, err := schedule.Run(ctx); !errors.Is(err, ErrDisabled) {
|
||||
t.Fatalf("disabled updater accepted a run: %v", err)
|
||||
}
|
||||
runs, err := database.RecentRuns(ctx, 10)
|
||||
if err != nil || len(runs) != 0 {
|
||||
t.Fatalf("disabled run must not create history: %#v err=%v", runs, err)
|
||||
}
|
||||
|
||||
schedule.nextRun.Store(time.Now().Add(time.Hour).Unix())
|
||||
schedule.Trigger()
|
||||
if !schedule.NextRunAt().IsZero() {
|
||||
t.Fatalf("trigger did not reset the visible countdown: %v", schedule.NextRunAt())
|
||||
}
|
||||
select {
|
||||
case <-schedule.wake:
|
||||
default:
|
||||
t.Fatal("trigger did not wake the scheduler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGloballyDisabledIPv6SkipsAAAATargetWithoutError(t *testing.T) {
|
||||
t.Parallel()
|
||||
schedule := &Scheduler{}
|
||||
changed, status, err := schedule.syncTarget(
|
||||
context.Background(),
|
||||
"unused-token",
|
||||
model.Zone{ProviderID: "123"},
|
||||
model.Target{RecordName: "home", UpdateIPv6: true},
|
||||
nil,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
)
|
||||
if err != nil || changed || status != "AAAA global deaktiviert" {
|
||||
t.Fatalf("disabled IPv6 was not skipped cleanly: changed=%v status=%q err=%v", changed, status, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncTargetTTLUpdatesExistingRRSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
changes := make(chan int, 1)
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/zones/123/rrsets":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"rrsets":[{"name":"home","type":"A","ttl":60,"records":[{"value":"198.51.100.11"}]}],"meta":{"pagination":{"next_page":null}}}`))
|
||||
case "/zones/123/rrsets/home/A/actions/change_ttl":
|
||||
var body struct {
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
changes <- body.TTL
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
default:
|
||||
http.Error(w, "unexpected request", http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
database, err := store.Open(ctx, filepath.Join(dir, "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
keyFile := filepath.Join(dir, "master-key")
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := secure.LoadCipher(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("test-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "test", ciphertext, nonce, cipher.Fingerprint("test-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "123", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
schedule := New(database, hetzner.New(api.URL), ipresolve.New(), cipher, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
if err := schedule.SyncTargetTTL(ctx, model.Target{ZoneID: zone.ID, RecordName: "home", UpdateIPv4: true, TTL: 300}); err != nil {
|
||||
t.Fatalf("sync target TTL: %v", err)
|
||||
}
|
||||
select {
|
||||
case ttl := <-changes:
|
||||
if ttl != 300 {
|
||||
t.Fatalf("unexpected changed TTL: %d", ttl)
|
||||
}
|
||||
default:
|
||||
t.Fatal("TTL action was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncStaticTargetWritesManualAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
actions := make(chan string, 2)
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/zones/123/rrsets":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"rrsets":[{"name":"home","type":"A","ttl":60,"records":[{"value":"198.51.100.11","comment":"Old"}]}],"meta":{"pagination":{"next_page":null}}}`))
|
||||
case "/zones/123/rrsets/home/A/actions/change_ttl", "/zones/123/rrsets/home/A/actions/set_records":
|
||||
actions <- r.URL.Path
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
default:
|
||||
http.Error(w, "unexpected request", http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
database, err := store.Open(ctx, filepath.Join(dir, "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
keyFile := filepath.Join(dir, "master-key")
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := secure.LoadCipher(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("test-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "test", ciphertext, nonce, cipher.Fingerprint("test-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "123", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
schedule := New(database, hetzner.New(api.URL), ipresolve.New(), cipher, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
changed, status, err := schedule.SyncStaticTarget(ctx, model.Target{ZoneID: zone.ID, RecordName: "home", UpdateIPv4: true, StaticIPv4: "203.0.113.25", Description: "Manual", TTL: 300})
|
||||
if err != nil || !changed || status != "A aktualisiert · TTL aktualisiert" {
|
||||
t.Fatalf("static target was not synchronized: changed=%v status=%q err=%v", changed, status, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for range 2 {
|
||||
seen[<-actions] = true
|
||||
}
|
||||
for _, expected := range []string{"/zones/123/rrsets/home/A/actions/change_ttl", "/zones/123/rrsets/home/A/actions/set_records"} {
|
||||
if !seen[expected] {
|
||||
t.Errorf("missing static update action %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncZoneTTLUpdatesManagedAddressAndCNAMERecords(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
actions := make(chan string, 2)
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/zones/123/rrsets":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"rrsets":[{"name":"home","type":"A","ttl":60,"records":[{"value":"198.51.100.11"}]},{"name":"www","type":"CNAME","ttl":300,"records":[{"value":"home.example.de."}]}],"meta":{"pagination":{"next_page":null}}}`))
|
||||
case "/zones/123/rrsets/home/A/actions/change_ttl", "/zones/123/rrsets/www/CNAME/actions/change_ttl":
|
||||
actions <- r.URL.Path
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
default:
|
||||
http.Error(w, "unexpected request", http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
database, err := store.Open(ctx, filepath.Join(dir, "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
keyFile := filepath.Join(dir, "master-key")
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := secure.LoadCipher(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("test-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "test", ciphertext, nonce, cipher.Fingerprint("test-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "123", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
schedule := New(database, hetzner.New(api.URL), ipresolve.New(), cipher, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
changed, err := schedule.SyncZoneTTL(ctx, zone.ID, []model.Target{{ZoneID: zone.ID, RecordName: "home", UpdateIPv4: true}}, []model.CNAMERecord{{ZoneID: zone.ID, Active: true, RecordName: "www"}}, 600)
|
||||
if err != nil || changed != 2 {
|
||||
t.Fatalf("zone TTL was not synchronized: changed=%d err=%v", changed, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for range 2 {
|
||||
seen[<-actions] = true
|
||||
}
|
||||
for _, expected := range []string{"/zones/123/rrsets/home/A/actions/change_ttl", "/zones/123/rrsets/www/CNAME/actions/change_ttl"} {
|
||||
if !seen[expected] {
|
||||
t.Errorf("missing bulk TTL action %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteTargetFamiliesAndGlobalIPv6Cleanup(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
deletions := make(chan string, 2)
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/zones/123/rrsets":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"rrsets":[{"name":"home","type":"A","ttl":60,"records":[{"value":"198.51.100.11"}]},{"name":"home","type":"AAAA","ttl":60,"records":[{"value":"2001:db8::11"}]}],"meta":{"pagination":{"next_page":null}}}`))
|
||||
case "/zones/123/rrsets/home/A", "/zones/123/rrsets/home/AAAA":
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "unexpected method", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
deletions <- r.URL.Path
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
http.Error(w, "unexpected request", http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
database, err := store.Open(ctx, filepath.Join(dir, "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
keyFile := filepath.Join(dir, "master-key")
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := secure.LoadCipher(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("test-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "test", ciphertext, nonce, cipher.Fingerprint("test-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "123", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := model.Target{ZoneID: zone.ID, RecordName: "home", UpdateIPv4: true, UpdateIPv6: true}
|
||||
|
||||
schedule := New(database, hetzner.New(api.URL), ipresolve.New(), cipher, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
deleted, err := schedule.DeleteTargetFamilies(ctx, target, true, false)
|
||||
if err != nil || deleted != 1 {
|
||||
t.Fatalf("A record was not deleted: deleted=%d err=%v", deleted, err)
|
||||
}
|
||||
deleted, err = schedule.DeleteIPv6Targets(ctx, []model.Target{target})
|
||||
if err != nil || deleted != 1 {
|
||||
t.Fatalf("global AAAA cleanup failed: deleted=%d err=%v", deleted, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for range 2 {
|
||||
seen[<-deletions] = true
|
||||
}
|
||||
for _, expected := range []string{"/zones/123/rrsets/home/A", "/zones/123/rrsets/home/AAAA"} {
|
||||
if !seen[expected] {
|
||||
t.Errorf("missing delete request %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteZoneRecordsRemovesOnlyLocallyRepresentedRRSets(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
deletions := make(chan string, 4)
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/zones/123/rrsets":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"rrsets":[
|
||||
{"name":"home","type":"A","ttl":60,"records":[{"value":"198.51.100.11"}]},
|
||||
{"name":"home","type":"AAAA","ttl":60,"records":[{"value":"2001:db8::11"}]},
|
||||
{"name":"legacy","type":"AAAA","ttl":60,"records":[{"value":"2001:db8::12"}]},
|
||||
{"name":"www","type":"CNAME","ttl":300,"records":[{"value":"home.example.de."}]},
|
||||
{"name":"mail","type":"CNAME","ttl":300,"records":[{"value":"external.example."}]},
|
||||
{"name":"@","type":"TXT","ttl":300,"records":[{"value":"unmanaged"}]}
|
||||
],"meta":{"pagination":{"next_page":null}}}`))
|
||||
case "/zones/123/rrsets/home/A", "/zones/123/rrsets/home/AAAA", "/zones/123/rrsets/legacy/AAAA", "/zones/123/rrsets/www/CNAME":
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "unexpected method", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
deletions <- r.URL.Path
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
http.Error(w, "unexpected request", http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
defer api.Close()
|
||||
|
||||
dir := t.TempDir()
|
||||
database, err := store.Open(ctx, filepath.Join(dir, "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
keyFile := filepath.Join(dir, "master-key")
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
if err := os.WriteFile(keyFile, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := secure.LoadCipher(keyFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("test-token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "test", ciphertext, nonce, cipher.Fingerprint("test-token"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "123", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
schedule := New(database, hetzner.New(api.URL), ipresolve.New(), cipher, slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||
deleted, err := schedule.DeleteZoneRecords(
|
||||
ctx,
|
||||
zone.ID,
|
||||
[]model.Target{{RecordName: "home"}, {RecordName: "legacy"}},
|
||||
[]model.CNAMERecord{{RecordName: "www"}},
|
||||
)
|
||||
if err != nil || deleted != 4 {
|
||||
t.Fatalf("managed zone records were not deleted: deleted=%d err=%v", deleted, err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for range 4 {
|
||||
seen[<-deletions] = true
|
||||
}
|
||||
for _, expected := range []string{"/zones/123/rrsets/home/A", "/zones/123/rrsets/home/AAAA", "/zones/123/rrsets/legacy/AAAA", "/zones/123/rrsets/www/CNAME"} {
|
||||
if !seen[expected] {
|
||||
t.Errorf("missing delete request %s", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user