carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "testing"
8 "time"
9)
10
11func TestTargetSetGet(t *testing.T) {
12 s := NewDirStore(t.TempDir())
13 ctx := context.Background()
14 if vt, etag, err := GetTarget(ctx, s, "acct1", "km"); err != nil || vt != nil || etag != "" {
15 t.Fatalf("unset target: got %v, %q, %v; want nil, \"\", nil", vt, etag, err)
16 }
17 now := time.Unix(1000, 0).UTC()
18 if err := SetTarget(ctx, s, "acct1", "km", "0.5.0", "paul@example.com", now); err != nil {
19 t.Fatal(err)
20 }
21 vt, etag, err := GetTarget(ctx, s, "acct1", "km")
22 if err != nil || vt == nil || vt.Target != "0.5.0" || vt.SetBy != "paul@example.com" || !vt.SetAt.Equal(now) {
23 t.Fatalf("round trip: %+v, %v", vt, err)
24 }
25 if etag == "" {
26 t.Fatal("etag on a hit must not be empty — CAS on update depends on it")
27 }
28 // Raising is fine; garbage and iterations are not.
29 if err := SetTarget(ctx, s, "acct1", "km", "0.6.0", "paul@example.com", now); err != nil {
30 t.Fatal(err)
31 }
32 for _, bad := range []string{"", "0.5", "v1.0.0", "0.5.0-3", "de2766f8"} {
33 if err := SetTarget(ctx, s, "acct1", "km", bad, "x", now); err == nil {
34 t.Errorf("SetTarget(%q) accepted", bad)
35 }
36 }
37}
38
39func TestTargetMustExceedTags(t *testing.T) {
40 s := NewDirStore(t.TempDir())
41 ctx := context.Background()
42 now := time.Unix(1000, 0).UTC()
43 tag := Tag{Schema: 1, Version: "0.5.0", Release: "0.5.0-3", TaggedAt: now}
44 if err := writeTag(ctx, s, "acct1", "km", &tag); err != nil {
45 t.Fatal(err)
46 }
47 if err := SetTarget(ctx, s, "acct1", "km", "0.5.0", "x", now); err == nil {
48 t.Fatal("target equal to an existing tag accepted")
49 }
50 if err := SetTarget(ctx, s, "acct1", "km", "0.4.9", "x", now); err == nil {
51 t.Fatal("target below an existing tag accepted")
52 }
53 if err := SetTarget(ctx, s, "acct1", "km", "0.5.1", "x", now); err != nil {
54 t.Fatalf("next patch refused: %v", err)
55 }
56}
57
58func TestNextIteration(t *testing.T) {
59 s := NewDirStore(t.TempDir())
60 ctx := context.Background()
61 tgt := SemVer{0, 5, 0}
62 v, err := NextIteration(ctx, s, "acct1", "km", tgt)
63 if err != nil || v != "0.5.0-1" {
64 t.Fatalf("first iteration: %q, %v", v, err)
65 }
66 // Existing releases under this target and noise from other schemes both
67 // sit in the same prefix; only this target's iterations count.
68 for _, ver := range []string{"0.5.0-1", "0.5.0-3", "0.4.0-9", "de2766f8"} {
69 if _, err := s.Put(ctx, ReleasesPrefix("acct1", "km")+ver+"/manifest.json", []byte("{}"), ""); err != nil {
70 t.Fatal(err)
71 }
72 }
73 v, err = NextIteration(ctx, s, "acct1", "km", tgt)
74 if err != nil || v != "0.5.0-4" {
75 t.Fatalf("after -1 and -3 exist: %q, %v; want 0.5.0-4", v, err)
76 }
77}
78
79// TestGetTargetValidates matches GetTag/validateProductionFlag's discipline:
80// a hit is checked before it is trusted, not handed back as-is.
81func TestGetTargetValidates(t *testing.T) {
82 ctx := context.Background()
83 cases := []struct {
84 name string
85 raw []byte
86 }{
87 {"garbage json", []byte("not json")},
88 {"unsupported schema", mustMarshal(t, VersionTarget{Schema: 2, Target: "0.5.0"})},
89 {"target not a bare semver", mustMarshal(t, VersionTarget{Schema: 1, Target: "not-semver"})},
90 }
91 for _, c := range cases {
92 t.Run(c.name, func(t *testing.T) {
93 s := NewDirStore(t.TempDir())
94 if _, err := s.Put(ctx, TargetKey("acct1", "km"), c.raw, ""); err != nil {
95 t.Fatal(err)
96 }
97 if vt, etag, err := GetTarget(ctx, s, "acct1", "km"); err == nil {
98 t.Fatalf("want error, got vt=%+v etag=%q", vt, etag)
99 }
100 })
101 }
102}
103
104func mustMarshal(t *testing.T, v any) []byte {
105 t.Helper()
106 b, err := json.Marshal(v)
107 if err != nil {
108 t.Fatal(err)
109 }
110 return b
111}
112
113// racingStore wraps a Store and, the first time its key of interest is Get,
114// sneaks in a competing write directly against the underlying store before
115// returning — simulating a second operator's write landing between the
116// caller's Get and its own Put. Everything else passes through unchanged.
117type racingStore struct {
118 Store
119 key string
120 raced bool
121 race func()
122}
123
124func (r *racingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
125 raw, etag, err := r.Store.Get(ctx, key)
126 if key == r.key && !r.raced {
127 r.raced = true
128 r.race()
129 }
130 return raw, etag, err
131}
132
133// TestSetTargetCASConflict is the insurance for setTargetUnchecked's CAS
134// rewrite: a competing write landing between SetTarget's read and its own
135// Put must surface as ErrCASConflict, not a silent overwrite.
136func TestSetTargetCASConflict(t *testing.T) {
137 base := NewDirStore(t.TempDir())
138 ctx := context.Background()
139 now := time.Unix(1000, 0).UTC()
140 key := TargetKey("acct1", "km")
141 rs := &racingStore{Store: base, key: key}
142 rs.race = func() {
143 // A second writer creates the target first, directly against the
144 // underlying store — bypassing rs so this doesn't re-trigger itself.
145 if err := SetTarget(ctx, base, "acct1", "km", "0.4.0", "other", now); err != nil {
146 t.Fatalf("competing write: %v", err)
147 }
148 }
149 err := SetTarget(ctx, rs, "acct1", "km", "0.5.0", "actor", now)
150 if !errors.Is(err, ErrCASConflict) {
151 t.Fatalf("want ErrCASConflict, got %v", err)
152 }
153}
154