package hostclaim import ( "context" "encoding/json" "errors" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestReadRefusesMalformed(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) obs, err := Read(ctx, s, testHost) if err != nil || !obs.Absent || obs.Host != testHost { t.Fatalf("absent read = %+v, %v", obs, err) } key, _ := Key(testHost) good := `{"v":2,"host":"` + testHost + `","gen":1,"op":{},"touched":{"complete":false}}` unknownChore := servingClaim(t, ctx, s).Claim unknownChore.Op = Op{Kind: KindParking, ID: "x"} unknownChore.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-replica", Region: "ie", Retirement: "r", Owner: "region:ie"}} if err := unknownChore.Validate(); err != nil { t.Fatalf("invalid fixture before changing chore kind: %v", err) } unknownChore.Obligations[0].Kind = "teleport" unknownChoreBody, err := json.Marshal(unknownChore) if err != nil { t.Fatal(err) } for name, body := range map[string]string{ "unknown field": `{"v":2,"host":"` + testHost + `","gen":1,"op":{},"touched":{"complete":false},"surprise":1}`, "trailing data": good + ` {"v":2}`, "wrong host": `{"v":2,"host":"other.bab.oncarlos.com","gen":1,"op":{},"touched":{"complete":false}}`, "free with chores": `{"v":2,"host":"` + testHost + `","gen":1,"op":{},"touched":{"complete":false},` + `"obligations":[{"id":"o","op":"x","kind":"park-local","box":"b","retirement":"r","owner":"box:b"}]}`, "unknown chore kind": string(unknownChoreBody), } { s2 := release.NewDirStore(t.TempDir()) if _, err := s2.Put(ctx, key, []byte(body), ""); err != nil { t.Fatal(err) } _, err := Read(ctx, s2, testHost) if name == "unknown chore kind" && (err == nil || !strings.Contains(err.Error(), `obligation kind "teleport"`)) { t.Errorf("%s: read = %v, want obligation-kind rejection", name, err) } if !errors.Is(err, ErrMalformed) { t.Errorf("%s: read = %v, want ErrMalformed", name, err) } } } func TestUpdateDerivesGenAndBindsHost(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) obs, _ := Read(ctx, s, testHost) noop := func(c *Claim) error { return nil } o1, err := Update(ctx, s, obs, noop) if err != nil || o1.Claim.Gen != 1 || o1.Claim.Host != testHost { t.Fatalf("first write = %+v, %v", o1, err) } if _, err := Update(ctx, s, obs, noop); !errors.Is(err, ErrConflict) { t.Fatalf("stale absent write = %v, want ErrConflict (C5a)", err) } if _, err := Update(ctx, s, o1, func(c *Claim) error { c.Gen = 99; return nil }); !errors.Is(err, ErrMalformed) { t.Fatalf("transition changing gen = %v", err) } if _, err := Update(ctx, s, o1, func(c *Claim) error { c.Host = "other.bab.oncarlos.com"; return nil }); !errors.Is(err, ErrMalformed) { t.Fatalf("transition changing host = %v", err) } o2, err := Update(ctx, s, o1, noop) if err != nil || o2.Claim.Gen != 2 { t.Fatalf("second write = %+v, %v", o2, err) } if _, err := Update(ctx, s, o1, noop); !errors.Is(err, ErrConflict) { t.Fatalf("stale etag write = %v, want ErrConflict", err) } } func TestUpdateRejectsNonAbsentObservationWithoutETag(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) obs := Observed{ Host: testHost, Claim: &Claim{V: 2, Host: testHost, Gen: 1}, Absent: false, ETag: "", } if err := obs.Claim.Validate(); err != nil { t.Fatalf("invalid test claim: %v", err) } transitionCalled := false _, err := Update(ctx, s, obs, func(c *Claim) error { transitionCalled = true return nil }) if !errors.Is(err, ErrMalformed) { t.Errorf("Update = %v, want ErrMalformed", err) } if transitionCalled { t.Error("transition called for observation without ETag") } after, err := Read(ctx, s, testHost) if err != nil || !after.Absent { t.Errorf("read after rejected update = %+v, %v; want absent", after, err) } }