| new file mode 100644 |
| index 000000000..e404832ff |
| --- /dev/null |
| +++ b/internal/hostclaim/create_test.go |
| @@ -0,0 +1,115 @@ |
| +package hostclaim |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func newHolder() NewHolder { |
| + return NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + testHost + ".json", |
| + Account: "bab", App: "jam", Host: testHost, Region: "ie", |
| + CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)} |
| +} |
| + |
| +var record = []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`) |
| + |
| +func TestCreateLifecycle(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) |
| + reg, _ := Read(ctx, s, testHost) |
| + o, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now)) |
| + if err != nil || o.Claim.Op.Kind != KindCreating || o.Claim.Op.Intent["record"] != string(record) { |
| + t.Fatalf("begin = %+v, %v", o, err) |
| + } |
| + if o.Claim.Holder.Location != "instances/"+testHost+"/"+o.Claim.Holder.Incarnation { |
| + t.Fatalf("location = %q", o.Claim.Holder.Location) |
| + } |
| + o, err = Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")) |
| + if err != nil || o.Claim.Op.Kind != KindServing || o.Claim.Op.Exec != "adi-knitting" || o.Claim.Holder.Stamp != o.Claim.Gen { |
| + t.Fatalf("complete = %+v, %v", o.Claim, err) |
| + } |
| + if len(o.Claim.Touched.Boxes) != 1 || o.Claim.Touched.Boxes[0] != "adi-knitting" { |
| + t.Fatalf("executor not recorded as touched: %+v", o.Claim.Touched) |
| + } |
| + if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); !errors.Is(err, ErrStaleOp) { |
| + t.Fatalf("replay = %v, want ErrStaleOp", err) |
| + } |
| + if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c2", "console", now)); !errors.Is(err, ErrConflict) { |
| + t.Fatalf("stale create = %v, want ErrConflict", err) |
| + } |
| +} |
| + |
| +func TestBeginCreateRefusesAnotherHostOrKey(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) |
| + reg, _ := Read(ctx, s, testHost) |
| + other := newHolder() |
| + other.Host = "other.bab.oncarlos.com" |
| + if _, err := Update(ctx, s, reg, BeginCreate(other, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("another host's create on this claim = %v", err) |
| + } |
| + badKey := newHolder() |
| + badKey.Key = "apps/zzz/jam/instances/" + testHost + ".json" |
| + if _, err := Update(ctx, s, reg, BeginCreate(badKey, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("mismatched record key = %v", err) |
| + } |
| + other2 := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T09:00:00Z"}`) |
| + if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), other2, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("record body with another CreatedAt = %v", err) |
| + } |
| + if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), []byte("not json"), "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("record body not JSON = %v", err) |
| + } |
| + wrongRegion := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"uk","created_at":"2026-10-04T01:00:00Z"}`) |
| + if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), wrongRegion, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("record body in another region = %v", err) |
| + } |
| + // A sidecar key cannot claim an instance-shaped host (P1, plan review round 2). |
| + sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/shop.json", Account: "bab", App: "jam", Name: "shop", |
| + Host: testHost, Region: "ie", CreatedAt: newHolder().CreatedAt} |
| + if _, err := Update(ctx, s, reg, BeginCreate(sc, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("sidecar key on a non-sidecar host = %v", err) |
| + } |
| +} |
| + |
| +func TestBeginCreateSidecar(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) |
| + host := "bot.jam.bab.sidecar.internal" |
| + sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/bot.json", Account: "bab", App: "jam", Name: "bot", |
| + Host: host, Region: "ie", CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)} |
| + body := []byte(`{"name":"bot","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`) |
| + reg, _ := Read(ctx, s, host) |
| + if _, err := Update(ctx, s, reg, BeginCreate(sc, body, "op_s1", "console", now)); err != nil { |
| + t.Fatalf("sidecar create = %v", err) |
| + } |
| + s2 := release.NewDirStore(t.TempDir()) |
| + reg2, _ := Read(ctx, s2, host) |
| + wrongName := []byte(`{"name":"other","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`) |
| + if _, err := Update(ctx, s2, reg2, BeginCreate(sc, wrongName, "op_s1", "console", now)); !errors.Is(err, ErrWrongState) { |
| + t.Fatalf("sidecar body with another name = %v", err) |
| + } |
| +} |
| + |
| +func TestCancelRetiresAndRefusesReuse(t *testing.T) { |
| + ctx := context.Background() |
| + s := release.NewDirStore(t.TempDir()) |
| + now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) |
| + reg, _ := Read(ctx, s, testHost) |
| + o, _ := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now)) |
| + inc := o.Claim.Holder.Incarnation |
| + o, err := Update(ctx, s, o, CancelCreate("op_c1", now)) |
| + if err != nil || !o.Claim.Free() || len(o.Claim.CancelledCreates) != 1 || o.Claim.RetiredIncarnations[0] != inc { |
| + t.Fatalf("cancel = %+v, %v", o.Claim, err) |
| + } |
| + if _, err := Update(ctx, s, o, BeginCreate(newHolder(), record, "op_c3", "console", now)); !errors.Is(err, ErrRetiredIncarnation) { |
| + t.Fatalf("reuse = %v, want ErrRetiredIncarnation", err) |
| + } |
| +} |