package domains import ( "context" "errors" "os" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestAttachClaimCreates(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() created, changed, err := AttachClaim(ctx, s, Claim{ Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live", CreatedAt: "2026-08-12T00:00:00Z", CreatedBy: "paul@tito.io", }) if err != nil || !created || !changed { t.Fatalf("AttachClaim: created=%v changed=%v err=%v, want true true nil", created, changed, err) } got, err := GetClaim(ctx, s, "tickets.example.com") if err != nil { t.Fatalf("GetClaim: %v", err) } if got.Schema != 1 || got.Host != "tickets.example.com" || got.Account != "bab" || got.App != "kass" || got.Route != "canary/live" || got.CreatedAt != "2026-08-12T00:00:00Z" || got.CreatedBy != "paul@tito.io" { t.Fatalf("GetClaim round-trip mismatch: %+v", got) } } func TestAttachClaimIdempotent(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live"} if _, _, err := AttachClaim(ctx, s, claim); err != nil { t.Fatal(err) } before, err := GetClaim(ctx, s, claim.Host) if err != nil { t.Fatal(err) } // changed=false is what makes this a NO-OP rather than a write: the door // audits and bumps control/serial off that answer, and a re-attach on // every router reload pass is the steady state, not the exception. created, changed, err := AttachClaim(ctx, s, claim) if err != nil || created || changed { t.Fatalf("AttachClaim re-attach: created=%v changed=%v err=%v, want false false nil", created, changed, err) } after, err := GetClaim(ctx, s, claim.Host) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(before, after) { t.Fatalf("object changed on idempotent re-attach: before=%+v after=%+v", before, after) } } func TestAttachClaimConvergesRoute(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live", CreatedAt: "2026-08-12T00:00:00Z", Wildcard: true, Catchall: true} if _, _, err := AttachClaim(ctx, s, claim); err != nil { t.Fatal(err) } // A converge repoints Route and nothing else: the caller's claim here // deliberately carries no CreatedAt and no Wildcard, and both stored // values must survive — a route repoint must never strip another // branch's TLS opt-in or restamp the claim's age. created=false but // changed=TRUE: a converge IS a store write, so the door must audit it // and announce it to the fleet. created, changed, err := AttachClaim(ctx, s, Claim{Host: claim.Host, Account: "bab", App: "kass", Route: "canary/next"}) if err != nil || created || !changed { t.Fatalf("AttachClaim route change: created=%v changed=%v err=%v, want false true nil", created, changed, err) } got, err := GetClaim(ctx, s, claim.Host) if err != nil { t.Fatal(err) } if got.Route != "canary/next" { t.Fatalf("Route = %q, want canary/next", got.Route) } if !got.Wildcard || !got.Catchall || got.CreatedAt != "2026-08-12T00:00:00Z" { t.Fatalf("converge must preserve Wildcard+Catchall+CreatedAt, got wildcard=%v catchall=%v created_at=%q", got.Wildcard, got.Catchall, got.CreatedAt) } } func TestAttachClaimOtherOwnerRefused(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live"} if _, _, err := AttachClaim(ctx, s, claim); err != nil { t.Fatal(err) } before, err := GetClaim(ctx, s, claim.Host) if err != nil { t.Fatal(err) } other := claim other.Account = "bad" if _, _, err := AttachClaim(ctx, s, other); !errors.Is(err, ErrHostUnavailable) { t.Fatalf("other account: err = %v, want ErrHostUnavailable", err) } otherApp := claim otherApp.App = "console" if _, _, err := AttachClaim(ctx, s, otherApp); !errors.Is(err, ErrHostUnavailable) { t.Fatalf("other app: err = %v, want ErrHostUnavailable", err) } after, err := GetClaim(ctx, s, claim.Host) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(before, after) { t.Fatalf("object changed on refused attach: before=%+v after=%+v", before, after) } } func TestAttachClaimNewerSchemaRefused(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() body := []byte(`{"schema":2,"host":"tickets.example.com","account":"bab","app":"kass"}`) if _, err := s.Put(ctx, ClaimKey("tickets.example.com"), body, ""); err != nil { t.Fatal(err) } _, _, err := AttachClaim(ctx, s, Claim{Host: "tickets.example.com", Account: "bab", App: "kass"}) if err == nil || !strings.Contains(err.Error(), "schema") { t.Fatalf("AttachClaim over newer schema: err = %v, want error mentioning schema", err) } got, _, gerr := s.Get(ctx, ClaimKey("tickets.example.com")) if gerr != nil { t.Fatal(gerr) } if string(got) != string(body) { t.Fatalf("object changed on newer-schema refusal: got %s", got) } } func TestDetachClaimRemoves(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass"} if _, _, err := AttachClaim(ctx, s, claim); err != nil { t.Fatal(err) } removed, err := DetachClaim(ctx, s, claim.Host, claim.Account, claim.App) if err != nil || !removed { t.Fatalf("DetachClaim: removed=%v err=%v, want true nil", removed, err) } if _, err := GetClaim(ctx, s, claim.Host); !errors.Is(err, os.ErrNotExist) { t.Fatalf("GetClaim after detach: err = %v, want os.ErrNotExist", err) } } func TestDetachClaimUnknownQuiet(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() removed, err := DetachClaim(ctx, s, "nope.example.com", "bab", "kass") if err != nil || removed { t.Fatalf("DetachClaim unknown: removed=%v err=%v, want false nil", removed, err) } } func TestDetachClaimOtherOwnerQuiet(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() claim := Claim{Host: "tickets.example.com", Account: "bab", App: "kass"} if _, _, err := AttachClaim(ctx, s, claim); err != nil { t.Fatal(err) } removed, err := DetachClaim(ctx, s, claim.Host, "bad", "console") if err != nil || removed { t.Fatalf("DetachClaim other owner: removed=%v err=%v, want false nil", removed, err) } if _, err := GetClaim(ctx, s, claim.Host); err != nil { t.Fatalf("GetClaim after other-owner detach: %v, want claim still present", err) } } func TestListClaimsSkipsGarbage(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if _, _, err := AttachClaim(ctx, s, Claim{Host: "a.example.com", Account: "bab", App: "kass"}); err != nil { t.Fatal(err) } if _, _, err := AttachClaim(ctx, s, Claim{Host: "b.example.com", Account: "bad", App: "console"}); err != nil { t.Fatal(err) } if _, err := s.Put(ctx, ClaimKey("garbage.example.com"), []byte("not json"), ""); err != nil { t.Fatal(err) } claims, err := ListClaims(ctx, s) if err != nil { t.Fatalf("ListClaims: %v", err) } if len(claims) != 2 { t.Fatalf("ListClaims returned %d claims, want 2: %+v", len(claims), claims) } hosts := map[string]bool{} for _, c := range claims { hosts[c.Host] = true } if !hosts["a.example.com"] || !hosts["b.example.com"] { t.Fatalf("ListClaims missing expected hosts: %+v", claims) } } func TestNormalizeAliasHost(t *testing.T) { cases := []struct { raw string want string ok bool }{ {"Tickets.Example.COM.", "tickets.example.com", true}, {"localhost", "", false}, {"a..b", "", false}, {"", "", false}, {strings.Repeat("a", 254), "", false}, } for _, c := range cases { got, ok := NormalizeAliasHost(c.raw) if ok != c.ok { t.Errorf("NormalizeAliasHost(%q) ok = %v, want %v", c.raw, ok, c.ok) continue } if ok && got != c.want { t.Errorf("NormalizeAliasHost(%q) = %q, want %q", c.raw, got, c.want) } } } func TestSetClaimCapabilities(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, _, err := AttachClaim(ctx, s, Claim{Host: "woodstar.app", Account: "bab", App: "woodstar-relay", Route: "woodstar-relay.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z"}); err != nil { t.Fatal(err) } // Set both capabilities. changed, err := SetClaimCapabilities(ctx, s, "woodstar.app", "bab", "woodstar-relay", true, true) if err != nil || !changed { t.Fatalf("set: changed=%v err=%v", changed, err) } got, err := GetClaim(ctx, s, "woodstar.app") if err != nil || !got.Wildcard || !got.Catchall { t.Fatalf("stored claim: %+v err=%v", got, err) } // Route, CreatedAt survive the capability write. if got.Route != "woodstar-relay.bab.oncarlos.com" || got.CreatedAt != "2026-08-13T00:00:00Z" { t.Fatalf("capability write must preserve Route+CreatedAt: %+v", got) } // Idempotent: same values = no write. changed, err = SetClaimCapabilities(ctx, s, "woodstar.app", "bab", "woodstar-relay", true, true) if err != nil || changed { t.Fatalf("no-op set: changed=%v err=%v", changed, err) } // Turning both off works and reports changed. changed, err = SetClaimCapabilities(ctx, s, "woodstar.app", "bab", "woodstar-relay", false, false) if err != nil || !changed { t.Fatalf("unset: changed=%v err=%v", changed, err) } } func TestSetClaimCapabilitiesRefusals(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, _, err := AttachClaim(ctx, s, Claim{Host: "woodstar.app", Account: "bab", App: "woodstar-relay", CreatedAt: "2026-08-13T00:00:00Z"}); err != nil { t.Fatal(err) } // Catchall without wildcard: refused, never stored. if _, err := SetClaimCapabilities(ctx, s, "woodstar.app", "bab", "woodstar-relay", false, true); !errors.Is(err, ErrCatchallNeedsWildcard) { t.Fatalf("want ErrCatchallNeedsWildcard, got %v", err) } if got, _ := GetClaim(ctx, s, "woodstar.app"); got.Catchall { t.Fatal("refused catchall must not be stored") } // Wrong owner: neutral refusal, same error the attach door uses. if _, err := SetClaimCapabilities(ctx, s, "woodstar.app", "bad", "other-app", true, false); !errors.Is(err, ErrHostUnavailable) { t.Fatalf("want ErrHostUnavailable, got %v", err) } // No claim at all: neutral, not a bare not-found (no existence oracle). if _, err := SetClaimCapabilities(ctx, s, "unclaimed.example", "bab", "woodstar-relay", true, false); !errors.Is(err, ErrHostUnavailable) { t.Fatalf("want ErrHostUnavailable for missing claim, got %v", err) } } func TestHostDenied(t *testing.T) { deny := []string{"oncarlos.com"} cases := []struct { host string want bool }{ {"x.oncarlos.com", true}, {"oncarlos.com", true}, {"xoncarlos.com", false}, } for _, c := range cases { if got := HostDenied(c.host, deny); got != c.want { t.Errorf("HostDenied(%q, %v) = %v, want %v", c.host, deny, got, c.want) } } if HostDenied("anything.example.com", []string{"", "oncarlos.com"}) { t.Fatal("empty deny entry must never match") } }