carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "context"
5 "errors"
6 "math"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10 "time"
11
12 "github.com/aws/aws-sdk-go-v2/aws"
13 "github.com/carlosframework/platform/internal/lease"
14 "github.com/carlosframework/platform/internal/lease/leasetest"
15)
16
17type fixedLease struct{ until time.Time }
18
19func (f fixedLease) SafeUntil() (time.Time, error) { return f.until, nil }
20
21type failedLease struct{ err error }
22
23func (f failedLease) SafeUntil() (time.Time, error) { return time.Time{}, f.err }
24
25// §7.3 "sign only before SafeUntil, and sign at the check": the guard
26// leaves room for the signature's whole valid life and the clock skew.
27func TestGuardedRefusesWithinTheSignatureLife(t *testing.T) {
28 t0 := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
29 g := Guarded{Lease: fixedLease{t0.Add(30 * time.Minute)}, Now: func() time.Time { return t0 }, Skew: time.Minute}
30 ran := 0
31 var got time.Time
32 if err := g.Before(func(at time.Time) error { ran++; got = at; return nil }); err != nil || ran != 1 || !got.Equal(t0) {
33 t.Fatalf("29 minutes of room: err %v ran %d signAt %v", err, ran, got)
34 }
35 g.Now = func() time.Time { return t0.Add(14*time.Minute + 1*time.Second) }
36 if err := g.Before(func(time.Time) error { ran++; return nil }); !errors.Is(err, ErrLeaseUnsafe) || ran != 1 {
37 t.Fatalf("inside the 15 min + skew window: err %v ran %d", err, ran)
38 }
39 if StagingLeasePrefix("uk", "a.go.example.com") != "control/staging-leases/uk/a.go.example.com" {
40 t.Fatal(StagingLeasePrefix("uk", "a.go.example.com"))
41 }
42 // The real settings: a fresh staging lease (45 min TTL) is usable for
43 // its first 29 minutes minus the renewal margin; the ordinary 15 min
44 // lease would never pass the guard, which is why StagingLeaseTTL exists.
45 if StagingLeaseTTL <= SignatureLife+DefaultSkew+15*time.Minute {
46 t.Fatalf("StagingLeaseTTL %v leaves no room for the signature life", StagingLeaseTTL)
47 }
48}
49
50// A lease that cannot answer SafeUntil (lost, spent) is unsafe, and fn
51// never runs.
52func TestGuardedRefusesALostLease(t *testing.T) {
53 g := Guarded{Lease: failedLease{lease.ErrLost}, Now: time.Now}
54 err := g.Before(func(time.Time) error { t.Fatal("ran on a lost lease"); return nil })
55 if !errors.Is(err, ErrLeaseUnsafe) {
56 t.Fatalf("lost lease: %v", err)
57 }
58}
59
60// The boundary is exact: now + life + skew == safe is allowed, one
61// nanosecond more is not. A zero Skew means DefaultSkew.
62func TestGuardedBoundary(t *testing.T) {
63 t0 := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
64 safe := t0.Add(SignatureLife + DefaultSkew)
65 ok := Guarded{Lease: fixedLease{safe}, Now: func() time.Time { return t0 }}
66 if err := ok.Before(func(time.Time) error { return nil }); err != nil {
67 t.Fatalf("exactly at the bound: %v", err)
68 }
69 late := Guarded{Lease: fixedLease{safe}, Now: func() time.Time { return t0.Add(1) }}
70 if err := late.Before(func(time.Time) error { return nil }); !errors.Is(err, ErrLeaseUnsafe) {
71 t.Fatalf("one ns past the bound: %v", err)
72 }
73 // fn's own error comes back untouched.
74 boom := errors.New("boom")
75 if err := ok.Before(func(time.Time) error { return boom }); err != boom {
76 t.Fatalf("fn error: %v", err)
77 }
78}
79
80// The real lease settings, by arithmetic: lease.Held's SafeUntil is
81// Until minus the renewal margin, so a fresh lease's SafeUntil is at most
82// TTL ahead. A fresh StagingLeaseTTL lease passes the guard; a fresh
83// lease.LeaseTTL lease never can, which is why StagingLeaseTTL exists.
84// lease.AcquireTTL itself is covered where lease's own Acquire tests are
85// (lease_test.go, S3-gated): this package adds no second S3 fixture.
86func TestGuardAgainstRealLeaseSettings(t *testing.T) {
87 t0 := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
88 fresh := func(ttl time.Duration) SafeUntilGuard {
89 return fixedLease{t0.Add(ttl - lease.RenewMargin)} // what Held.SafeUntil answers right after Acquire
90 }
91 if err := (Guarded{Lease: fresh(StagingLeaseTTL), Now: func() time.Time { return t0 }}).Before(func(time.Time) error { return nil }); err != nil {
92 t.Fatalf("a fresh staging lease is refused: %v", err)
93 }
94 if err := (Guarded{Lease: fresh(lease.LeaseTTL), Now: func() time.Time { return t0 }}).Before(func(time.Time) error { return nil }); !errors.Is(err, ErrLeaseUnsafe) {
95 t.Fatalf("a fresh 15 minute lease passed the guard: %v", err)
96 }
97}
98
99// The staging mirror signs at the check and never again: a request
100// carries the X-Amz-Date of the timestamp Guarded.Before captured, and a
101// second attempt is a second Before. The fixture is a recording handler
102// (the test asserts the signed header, not AWS's acceptance of it) in
103// front of internal/lease/leasetest's local S3, which stores a plain PUT
104// as a new version beside its create-only PUT.
105func TestStagingMirrorSignsAtTheCheck(t *testing.T) {
106 bucket := leasetest.NewVersionedBucket()
107 bucket.AddBucket("data-uk")
108 handler, _ := leasetest.NewS3Server(bucket, "data-uk")
109 var dates []string
110 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
111 dates = append(dates, r.Header.Get("X-Amz-Date"))
112 handler.ServeHTTP(w, r)
113 }))
114 defer srv.Close()
115 creds := aws.CredentialsProviderFunc(func(context.Context) (aws.Credentials, error) {
116 return aws.Credentials{AccessKeyID: "AKIATEST", SecretAccessKey: "secret"}, nil
117 })
118 m := lease.NewStagingMirrorAt(srv.URL, "eu-west-2", creds)
119 t0 := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
120 now := t0
121 g := Guarded{Lease: fixedLease{t0.Add(30 * time.Minute)}, Now: func() time.Time { return now }}
122 // First write: checked and signed at t0.
123 if err := g.Before(func(at time.Time) error {
124 _, err := m.PutVersionedAt(context.Background(), at, "data-uk", "moves/h/mv/timers/t.json", []byte("{}"))
125 return err
126 }); err != nil {
127 t.Fatal(err)
128 }
129 // Second write: the check happens at t0+3m; then the goroutine pauses
130 // past every bound before the request is signed and sent. The
131 // signature must carry the CHECK's time, so no pause can mint a fresh
132 // signature after the lease is gone (round-4 finding 3).
133 now = t0.Add(3 * time.Minute)
134 if err := g.Before(func(at time.Time) error {
135 now = t0.Add(40 * time.Minute) // the pause: the wall clock is past the lease
136 _, err := m.PutVersionedAt(context.Background(), at, "data-uk", "moves/h/mv/timers/t.json", []byte("{}"))
137 return err
138 }); err != nil {
139 t.Fatal(err)
140 }
141 if len(dates) != 2 || dates[0] != "20261006T090000Z" || dates[1] != "20261006T090300Z" {
142 t.Fatalf("X-Amz-Date per request must be the check's time: %v", dates)
143 }
144 // The lease is now unsafe: a third Before refuses and sends nothing.
145 if err := g.Before(func(time.Time) error { t.Fatal("signed after the bound"); return nil }); !errors.Is(err, ErrLeaseUnsafe) || len(dates) != 2 {
146 t.Fatalf("after the bound: %v, %d requests", err, len(dates))
147 }
148 // create-only through the same signed path
149 now = t0.Add(4 * time.Minute)
150 put := func() error {
151 return g.Before(func(at time.Time) error {
152 _, err := m.PutIfAbsentVersionedAt(context.Background(), at, "data-uk", "moves/h/mv/move.json", []byte("{}"))
153 return err
154 })
155 }
156 if err := put(); err != nil {
157 t.Fatal(err)
158 }
159 if err := put(); !errors.Is(err, lease.ErrExists) {
160 t.Fatalf("second create-only put: %v", err)
161 }
162}
163
164// A negative skew would widen the window the guard is there to close (a
165// -15 minute skew admits a signature that outlives the lease), so it is a
166// configuration error and fn never runs. A skew so large that
167// SignatureLife+Skew overflows must refuse, not wrap into a pass.
168func TestGuardedSkewIsValidated(t *testing.T) {
169 t0 := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
170 plenty := fixedLease{t0.Add(24 * time.Hour)}
171 ran := false
172 fn := func(time.Time) error { ran = true; return nil }
173 neg := Guarded{Lease: fixedLease{t0.Add(time.Second)}, Now: func() time.Time { return t0 }, Skew: -15 * time.Minute}
174 if err := neg.Before(fn); !errors.Is(err, ErrInvalidSkew) || ran {
175 t.Fatalf("negative skew: err %v ran %v", err, ran)
176 }
177 neg.Lease = plenty
178 if err := neg.Before(fn); !errors.Is(err, ErrInvalidSkew) || ran {
179 t.Fatalf("negative skew with room: err %v ran %v", err, ran)
180 }
181 for _, skew := range []time.Duration{time.Duration(math.MaxInt64), time.Duration(math.MaxInt64) - SignatureLife + 1} {
182 huge := Guarded{Lease: plenty, Now: func() time.Time { return t0 }, Skew: skew}
183 if err := huge.Before(fn); !errors.Is(err, ErrLeaseUnsafe) || ran {
184 t.Fatalf("skew %v: err %v ran %v", skew, err, ran)
185 }
186 }
187}
188