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
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,294 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/model"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/secure"
|
||||
)
|
||||
|
||||
func TestUserAccessCanBeScopedToTargetOrDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
hash, err := secure.HashPassword("a secure password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.CreateAdmin(ctx, "admin", "Administrator", hash); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
member, err := database.CreateUser(ctx, "alice", "Alice", hash, "member")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credential, err := database.CreateCredential(ctx, "Hetzner", []byte("ciphertext"), []byte("nonce"), "fingerprint")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "10", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
home, err := database.CreateTarget(ctx, model.Target{ZoneID: zone.ID, RecordName: "home", FQDN: "home.example.de", UpdateIPv4: true, Active: true, StaticIPv4: "203.0.113.10", TTL: 60})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if home.StaticIPv4 != "203.0.113.10" {
|
||||
t.Fatalf("static IPv4 was not persisted: %#v", home)
|
||||
}
|
||||
other, err := database.CreateTarget(ctx, model.Target{ZoneID: zone.ID, RecordName: "other", FQDN: "other.example.de", UpdateIPv4: true, TTL: 60})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := database.ReplaceAccessGrants(ctx, member.ID, AccessSelection{TargetIDs: []int64{home.ID}, ManageTargetIDs: []int64{home.ID}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
allowed, err := database.CanAccessTarget(ctx, member.ID, zone.ID, home.ID)
|
||||
if err != nil || !allowed {
|
||||
t.Fatalf("target-specific grant was not honored: allowed=%v err=%v", allowed, err)
|
||||
}
|
||||
allowed, err = database.CanAccessTarget(ctx, member.ID, zone.ID, other.ID)
|
||||
if err != nil || allowed {
|
||||
t.Fatalf("ungranted target was visible: allowed=%v err=%v", allowed, err)
|
||||
}
|
||||
managed, err := database.CanManageTarget(ctx, member.ID, zone.ID, home.ID)
|
||||
if err != nil || !managed {
|
||||
t.Fatalf("target management grant was not honored: managed=%v err=%v", managed, err)
|
||||
}
|
||||
targets, err := database.ListAccessibleTargets(ctx, member.ID, zone.ID)
|
||||
if err != nil || len(targets) != 1 || targets[0].ID != home.ID {
|
||||
t.Fatalf("expected one scoped target, got %#v (err=%v)", targets, err)
|
||||
}
|
||||
|
||||
if err := database.ReplaceAccessGrants(ctx, member.ID, AccessSelection{ZoneIDs: []int64{zone.ID}, ManageZoneIDs: []int64{zone.ID}, CreateTargetZoneIDs: []int64{zone.ID}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
targets, err = database.ListAccessibleTargets(ctx, member.ID, zone.ID)
|
||||
if err != nil || len(targets) != 2 {
|
||||
t.Fatalf("domain grant did not include all targets: %d, %v", len(targets), err)
|
||||
}
|
||||
canCreate, err := database.CanCreateTarget(ctx, member.ID, zone.ID)
|
||||
if err != nil || !canCreate {
|
||||
t.Fatalf("domain create grant was not honored: canCreate=%v err=%v", canCreate, err)
|
||||
}
|
||||
|
||||
if err := database.ArchiveTarget(ctx, home.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linked, err := database.ListTargets(ctx, zone.ID)
|
||||
if err != nil || len(linked) != 1 || linked[0].ID != other.ID {
|
||||
t.Fatalf("archived target remained linked: %#v, %v", linked, err)
|
||||
}
|
||||
archived, err := database.ListArchivedTargets(ctx, zone.ID)
|
||||
if err != nil || len(archived) != 1 || archived[0].ID != home.ID || archived[0].ArchivedAt.IsZero() {
|
||||
t.Fatalf("archived target was not retained: %#v, %v", archived, err)
|
||||
}
|
||||
imported, err := database.UpsertImportedTarget(ctx, model.Target{ZoneID: zone.ID, RecordName: "home", FQDN: "home.example.de", UpdateIPv4: true, TTL: 60})
|
||||
if err != nil || imported {
|
||||
t.Fatalf("provider re-import unexpectedly restored an archived target: imported=%v err=%v", imported, err)
|
||||
}
|
||||
if err := database.RestoreTarget(ctx, home.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restored, err := database.GetTarget(ctx, home.ID)
|
||||
if err != nil || !restored.ArchivedAt.IsZero() || restored.Active {
|
||||
t.Fatalf("target was not safely restored as inactive: %#v, %v", restored, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInactiveUserSessionIsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hash, _ := secure.HashPassword("a secure password")
|
||||
user, err := database.CreateUser(ctx, "member", "Member", hash, "member")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session, err := database.CreateSession(ctx, model.Session{UserID: user.ID, TokenHash: "token", CSRFToken: "csrf", ExpiresAt: time.Now().Add(time.Hour)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := database.GetSession(ctx, session.TokenHash)
|
||||
if err != nil || loaded == nil || loaded.Username != "member" {
|
||||
t.Fatalf("session was not resolved with user: %#v, %v", loaded, err)
|
||||
}
|
||||
if err := database.UpdateUserState(ctx, user.ID, "Member", "member", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err = database.GetSession(ctx, session.TokenHash)
|
||||
if err != nil || loaded != nil {
|
||||
t.Fatalf("inactive user session was accepted: %#v, %v", loaded, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPv6TargetsCanBeDisabledAndTargetCanBeDeleted(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
credential, err := database.CreateCredential(ctx, "Hetzner", []byte("ciphertext"), []byte("nonce"), "ipv6-fingerprint")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "40", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := database.CreateTarget(ctx, model.Target{ZoneID: zone.ID, RecordName: "home", FQDN: "home.example.de", UpdateIPv4: true, UpdateIPv6: true, StaticIPv6: "2001:db8::10", TTL: 60})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
targets, err := database.ListTargetsWithIPv6(ctx)
|
||||
if err != nil || len(targets) != 1 || targets[0].ID != target.ID {
|
||||
t.Fatalf("IPv6 target was not listed: %#v err=%v", targets, err)
|
||||
}
|
||||
if err := database.DisableIPv6ForTargets(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err = database.GetTarget(ctx, target.ID)
|
||||
if err != nil || target.UpdateIPv6 || target.StaticIPv6 != "" {
|
||||
t.Fatalf("IPv6 target was not disabled cleanly: %#v err=%v", target, err)
|
||||
}
|
||||
if err := database.DeleteTarget(ctx, target.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := database.GetTarget(ctx, target.ID); err == nil {
|
||||
t.Fatal("deleted target was still found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdaterSettingsAndErrorsCanBeReset(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := 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 !settings.UpdaterEnabled || !settings.IPv6Enabled {
|
||||
t.Fatalf("new installations must enable updater and IPv6 by default: %#v", settings)
|
||||
}
|
||||
if err := database.UpdateSettings(ctx, 600, false, false, []string{"https://v4.example"}, []string{"https://v6.example"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings, err = database.GetSettings(ctx)
|
||||
if err != nil || settings.UpdaterEnabled || settings.IPv6Enabled || settings.UpdateIntervalSeconds != 600 {
|
||||
t.Fatalf("updater settings were not persisted: %#v err=%v", settings, err)
|
||||
}
|
||||
|
||||
credential, err := database.CreateCredential(ctx, "Hetzner", []byte("ciphertext"), []byte("nonce"), "error-fingerprint")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "20", "errors.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := database.CreateTarget(ctx, model.Target{ZoneID: zone.ID, RecordName: "home", FQDN: "home.errors.example", UpdateIPv4: true, Active: true, TTL: 60})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.UpdateTargetResult(ctx, target.ID, "Fehler", "provider rejected update", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.MarkZoneSynced(ctx, zone.ID, "zone unavailable"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runID, err := database.CreateUpdateRun(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.FinishUpdateRun(ctx, runID, "partial", "198.51.100.1", "", "one error"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.UpdateRunState(ctx, "198.51.100.1", "", "partial"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count, err := database.CountErrors(ctx); err != nil || count != 3 {
|
||||
t.Fatalf("unexpected error count: count=%d err=%v", count, err)
|
||||
}
|
||||
if err := database.ClearErrors(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count, err := database.CountErrors(ctx); err != nil || count != 0 {
|
||||
t.Fatalf("errors were not cleared: count=%d err=%v", count, err)
|
||||
}
|
||||
clearedTarget, err := database.GetTarget(ctx, target.ID)
|
||||
if err != nil || clearedTarget.LastError != "" || clearedTarget.LastStatus != "Zurückgesetzt" {
|
||||
t.Fatalf("target error was not reset: %#v err=%v", clearedTarget, err)
|
||||
}
|
||||
settings, err = database.GetSettings(ctx)
|
||||
if err != nil || settings.LastRunStatus != "" {
|
||||
t.Fatalf("last run error state was not reset: %#v err=%v", settings, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCNAMERecordsCanBeStoredAndReplacedFromProviderImport(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
credential, err := database.CreateCredential(ctx, "Hetzner", []byte("ciphertext"), []byte("nonce"), "cname-fingerprint")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := database.CreateZone(ctx, credential.ID, "30", "example.de")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
created, err := database.CreateCNAMERecord(ctx, model.CNAMERecord{ZoneID: zone.ID, Active: true, RecordName: "www", FQDN: "www.example.de", Destination: "app.example.net.", Description: "Website", TTL: 300})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
created.Destination = "edge.example.net."
|
||||
created.TTL = 600
|
||||
if err := database.UpdateCNAMERecord(ctx, created); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
updated, err := database.GetCNAMERecord(ctx, created.ID)
|
||||
if err != nil || !updated.Active || updated.Destination != "edge.example.net." || updated.TTL != 600 {
|
||||
t.Fatalf("CNAME update not persisted: %#v, %v", updated, err)
|
||||
}
|
||||
|
||||
if err := database.ReplaceCNAMERecords(ctx, zone.ID, []model.CNAMERecord{{ZoneID: zone.ID, RecordName: "cdn", FQDN: "cdn.example.de", Destination: "assets.example.net.", TTL: 120}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
records, err := database.ListCNAMERecords(ctx, zone.ID)
|
||||
if err != nil || len(records) != 2 {
|
||||
t.Fatalf("provider CNAME import was not mirrored: %#v, %v", records, err)
|
||||
}
|
||||
byName := make(map[string]model.CNAMERecord, len(records))
|
||||
for _, record := range records {
|
||||
byName[record.RecordName] = record
|
||||
}
|
||||
if byName["www"].Active || !byName["cdn"].Active || byName["cdn"].Destination != "assets.example.net." || byName["cdn"].TTL != 120 {
|
||||
t.Fatalf("inactive/local CNAME lifecycle was not preserved: %#v", byName)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user