package release import ( "context" "encoding/json" "errors" "testing" "time" ) func TestTargetSetGet(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() if vt, etag, err := GetTarget(ctx, s, "acct1", "km"); err != nil || vt != nil || etag != "" { t.Fatalf("unset target: got %v, %q, %v; want nil, \"\", nil", vt, etag, err) } now := time.Unix(1000, 0).UTC() if err := SetTarget(ctx, s, "acct1", "km", "0.5.0", "paul@example.com", now); err != nil { t.Fatal(err) } vt, etag, err := GetTarget(ctx, s, "acct1", "km") if err != nil || vt == nil || vt.Target != "0.5.0" || vt.SetBy != "paul@example.com" || !vt.SetAt.Equal(now) { t.Fatalf("round trip: %+v, %v", vt, err) } if etag == "" { t.Fatal("etag on a hit must not be empty — CAS on update depends on it") } // Raising is fine; garbage and iterations are not. if err := SetTarget(ctx, s, "acct1", "km", "0.6.0", "paul@example.com", now); err != nil { t.Fatal(err) } for _, bad := range []string{"", "0.5", "v1.0.0", "0.5.0-3", "de2766f8"} { if err := SetTarget(ctx, s, "acct1", "km", bad, "x", now); err == nil { t.Errorf("SetTarget(%q) accepted", bad) } } } func TestTargetMustExceedTags(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() now := time.Unix(1000, 0).UTC() tag := Tag{Schema: 1, Version: "0.5.0", Release: "0.5.0-3", TaggedAt: now} if err := writeTag(ctx, s, "acct1", "km", &tag); err != nil { t.Fatal(err) } if err := SetTarget(ctx, s, "acct1", "km", "0.5.0", "x", now); err == nil { t.Fatal("target equal to an existing tag accepted") } if err := SetTarget(ctx, s, "acct1", "km", "0.4.9", "x", now); err == nil { t.Fatal("target below an existing tag accepted") } if err := SetTarget(ctx, s, "acct1", "km", "0.5.1", "x", now); err != nil { t.Fatalf("next patch refused: %v", err) } } func TestNextIteration(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() tgt := SemVer{0, 5, 0} v, err := NextIteration(ctx, s, "acct1", "km", tgt) if err != nil || v != "0.5.0-1" { t.Fatalf("first iteration: %q, %v", v, err) } // Existing releases under this target and noise from other schemes both // sit in the same prefix; only this target's iterations count. for _, ver := range []string{"0.5.0-1", "0.5.0-3", "0.4.0-9", "de2766f8"} { if _, err := s.Put(ctx, ReleasesPrefix("acct1", "km")+ver+"/manifest.json", []byte("{}"), ""); err != nil { t.Fatal(err) } } v, err = NextIteration(ctx, s, "acct1", "km", tgt) if err != nil || v != "0.5.0-4" { t.Fatalf("after -1 and -3 exist: %q, %v; want 0.5.0-4", v, err) } } // TestGetTargetValidates matches GetTag/validateProductionFlag's discipline: // a hit is checked before it is trusted, not handed back as-is. func TestGetTargetValidates(t *testing.T) { ctx := context.Background() cases := []struct { name string raw []byte }{ {"garbage json", []byte("not json")}, {"unsupported schema", mustMarshal(t, VersionTarget{Schema: 2, Target: "0.5.0"})}, {"target not a bare semver", mustMarshal(t, VersionTarget{Schema: 1, Target: "not-semver"})}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { s := NewDirStore(t.TempDir()) if _, err := s.Put(ctx, TargetKey("acct1", "km"), c.raw, ""); err != nil { t.Fatal(err) } if vt, etag, err := GetTarget(ctx, s, "acct1", "km"); err == nil { t.Fatalf("want error, got vt=%+v etag=%q", vt, etag) } }) } } func mustMarshal(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatal(err) } return b } // racingStore wraps a Store and, the first time its key of interest is Get, // sneaks in a competing write directly against the underlying store before // returning — simulating a second operator's write landing between the // caller's Get and its own Put. Everything else passes through unchanged. type racingStore struct { Store key string raced bool race func() } func (r *racingStore) Get(ctx context.Context, key string) ([]byte, string, error) { raw, etag, err := r.Store.Get(ctx, key) if key == r.key && !r.raced { r.raced = true r.race() } return raw, etag, err } // TestSetTargetCASConflict is the insurance for setTargetUnchecked's CAS // rewrite: a competing write landing between SetTarget's read and its own // Put must surface as ErrCASConflict, not a silent overwrite. func TestSetTargetCASConflict(t *testing.T) { base := NewDirStore(t.TempDir()) ctx := context.Background() now := time.Unix(1000, 0).UTC() key := TargetKey("acct1", "km") rs := &racingStore{Store: base, key: key} rs.race = func() { // A second writer creates the target first, directly against the // underlying store — bypassing rs so this doesn't re-trigger itself. if err := SetTarget(ctx, base, "acct1", "km", "0.4.0", "other", now); err != nil { t.Fatalf("competing write: %v", err) } } err := SetTarget(ctx, rs, "acct1", "km", "0.5.0", "actor", now) if !errors.Is(err, ErrCASConflict) { t.Fatalf("want ErrCASConflict, got %v", err) } }