package hostclaim import ( "context" "encoding/json" "errors" "reflect" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) const testHost = "shop.bab.oncarlos.com" func TestKeyRefusesBadHosts(t *testing.T) { if k, err := Key(testHost); err != nil || k != "control/hosts/"+testHost+".json" { t.Fatalf("Key = %q, %v", k, err) } for _, bad := range []string{"", "../x", "a/b", "UPPER.example.com"} { if _, err := Key(bad); err == nil { t.Errorf("Key(%q) accepted", bad) } } } func TestIncarnationStableAndLegacyDistinct(t *testing.T) { at := time.Date(2026, 10, 4, 1, 2, 3, 4, time.UTC) a1, err := Incarnation("bab", "jam", testHost, at) if err != nil { t.Fatal(err) } a2, _ := Incarnation("bab", "jam", testHost, at) if a1 != a2 || len(a1) != 64 { t.Fatalf("not stable 64-hex: %q %q", a1, a2) } l1, err := Incarnation("bab", "jam", testHost, time.Time{}) if err != nil { t.Fatal(err) } l2, _ := Incarnation("bab", "other", testHost, time.Time{}) if l1 == a1 || l1 == l2 || len(l1) != 64 { t.Fatalf("legacy identity not distinct: %q %q %q", l1, l2, a1) } } func TestValidate(t *testing.T) { created := time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC) inc, _ := Incarnation("bab", "jam", testHost, created) ok := &Claim{V: 2, Host: testHost, Gen: 1, Holder: &Holder{Family: "instance", Key: "apps/bab/jam/instances/" + testHost + ".json", Account: "bab", App: "jam", Incarnation: inc, CreatedAt: created, Location: "instances/" + testHost + "/" + inc}, Op: Op{Kind: KindServing}} if err := ok.Validate(); err != nil { t.Fatalf("valid claim: %v", err) } bad := map[string]func(c *Claim){ "holderless serving": func(c *Claim) { c.Holder = nil }, "unknown kind": func(c *Claim) { c.Op = Op{Kind: "teleporting", ID: "x"} }, "foreign location": func(c *Claim) { c.Holder.Location = "instances/other.example.com" }, "short incarnation": func(c *Claim) { c.Holder.Incarnation = "abc" }, "wrong incarnation": func(c *Claim) { c.Holder.Incarnation = strings.Repeat("a", 64) }, "foreign key": func(c *Claim) { c.Holder.Key = "apps/zzz/jam/instances/" + testHost + ".json" }, "free with chores": func(c *Claim) { c.Holder, c.Op = nil, Op{} c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-local", Box: "b"}} }, "unknown chore kind": func(c *Claim) { c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "teleport", Region: "ie", Retirement: "r", Owner: "region:ie"}} }, "locationless chore": func(c *Claim) { c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-replica", Retirement: "r", Owner: "region:"}} }, "owner mismatch": func(c *Claim) { c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-local", Box: "b", Retirement: "r", Owner: "box:other"}} }, "duplicate chore id": func(c *Claim) { ob := Obligation{ID: "o", Op: "x", Kind: "park-local", Box: "b", Retirement: "r", Owner: "box:b"} c.Obligations = []Obligation{ob, ob} }, "old schema": func(c *Claim) { c.V = 1 }, } for name, mut := range bad { raw, _ := json.Marshal(ok) var c Claim _ = json.Unmarshal(raw, &c) mut(&c) if err := c.Validate(); err == nil { t.Errorf("%s validated", name) } } } func TestParkGenerationValidation(t *testing.T) { for _, name := range []string{"valid", "no region", "no generation", "box", "wrong owner", "no owner", "park-replica", "park-local"} { t.Run(name, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) before := servingClaim(t, ctx, s) ob := Obligation{ID: "o1", Op: "m1", Kind: "park-generation", Region: "ie", Generation: "dead_gen", Retirement: "r1", Owner: "region:ie"} switch name { case "no region": ob.Region, ob.Owner = "", "region:" case "no generation": ob.Generation = "" case "box": ob.Box = "boxS" case "wrong owner": ob.Owner = "region:uk" case "no owner": ob.Owner = "" case "park-replica": ob.Kind = name case "park-local": ob.Kind, ob.Region, ob.Box, ob.Owner = name, "", "boxS", "box:boxS" } next, err := Update(ctx, s, before, func(c *Claim) error { c.Obligations = []Obligation{ob}; return nil }) if name != "valid" { if !errors.Is(err, ErrMalformed) { t.Fatalf("invalid obligation = %v, want ErrMalformed", err) } assertAdmissionStored(t, ctx, s, before) return } if err != nil { t.Fatal(err) } stored := assertAdmissionStored(t, ctx, s, next) if !reflect.DeepEqual(stored.Claim.Obligations, []Obligation{ob}) { t.Fatalf("stored obligations = %+v", stored.Claim.Obligations) } raw, err := json.Marshal(ob) if err != nil { t.Fatal(err) } if !strings.Contains(string(raw), `"generation":"dead_gen"`) { t.Fatalf("generation wire form = %s", raw) } ob.Generation = "" raw, err = json.Marshal(ob) if err != nil { t.Fatal(err) } if strings.Contains(string(raw), `"generation"`) { t.Fatalf("empty generation not omitted: %s", raw) } }) } }