carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package lease_test
2
3import (
4 "context"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/lease"
12 "github.com/carlosframework/platform/internal/lease/leasetest"
13)
14
15// lockFixture points the package's S3 clients at leasetest's local store,
16// with credentials the AWS chain resolves without touching the machine's.
17func lockFixture(t *testing.T) *leasetest.VersionedBucket {
18 t.Helper()
19 bucket := leasetest.NewVersionedBucket()
20 bucket.AddBucket("data")
21 handler, _ := leasetest.NewS3Server(bucket)
22 srv := httptest.NewServer(handler)
23 t.Cleanup(srv.Close)
24 t.Setenv("AWS_ACCESS_KEY_ID", "test-key")
25 t.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
26 t.Setenv("AWS_SESSION_TOKEN", "")
27 t.Setenv("AWS_EC2_METADATA_DISABLED", "true")
28 t.Setenv("AWS_CONFIG_FILE", t.TempDir()+"/absent")
29 t.Setenv("AWS_SHARED_CREDENTIALS_FILE", t.TempDir()+"/absent")
30 t.Setenv("CARLOS_S3_ENDPOINT", srv.URL)
31 return bucket
32}
33
34func TestLockStateFollowsAcquireAndRelease(t *testing.T) {
35 lockFixture(t)
36 ctx := context.Background()
37 held, err := lease.Acquire(ctx, "data", "instances/h.example.com", "test-region")
38 if err != nil {
39 t.Fatal(err)
40 }
41 t.Cleanup(func() { _ = held.Release(context.Background()) })
42 got, _, expires, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region")
43 if err != nil || !got || !expires.After(time.Now()) {
44 t.Fatalf("after acquire: held %v expires %v err %v, want held with an expiry in the future", got, expires, err)
45 }
46 // ReplicaSource's prefix carries a trailing slash; the lock is the same.
47 if got, _, _, err := lease.LockState(ctx, "data", "instances/h.example.com/", "test-region"); err != nil || !got {
48 t.Fatalf("trailing slash: held %v err %v", got, err)
49 }
50 if err := held.Release(ctx); err != nil {
51 t.Fatal(err)
52 }
53 if got, owner, expires, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region"); err != nil || got || owner != "" || !expires.IsZero() {
54 t.Fatalf("after release: held %v owner %q expires %v err %v", got, owner, expires, err)
55 }
56}
57
58func TestLockStateAbsentPrefixIsNotHeld(t *testing.T) {
59 lockFixture(t)
60 got, owner, expires, err := lease.LockState(context.Background(), "data", "instances/never.example.com/inc-1", "test-region")
61 if err != nil || got || owner != "" || !expires.IsZero() {
62 t.Fatalf("absent: held %v owner %q expires %v err %v", got, owner, expires, err)
63 }
64}
65
66// §5.3 step 4: a lock counts as released only once it has expired by more
67// than the lease's renewal margin, so a holder whose clock runs a little
68// behind is still counted as holding.
69func TestLockStateExpiredWithinTheMarginIsHeld(t *testing.T) {
70 bucket := lockFixture(t)
71 ctx := context.Background()
72 put := func(loc string, expires time.Time) {
73 bucket.Put("data", loc+"/lock.json", []byte(`{"generation":3,"expires_at":"`+expires.UTC().Format(time.RFC3339Nano)+`","owner":"box-a"}`))
74 }
75 recent := time.Now().Add(-lease.RenewMargin / 2)
76 put("instances/recent.example.com", recent)
77 got, owner, expires, err := lease.LockState(ctx, "data", "instances/recent.example.com", "test-region")
78 if err != nil || !got || owner != "box-a" || !expires.Equal(recent) {
79 t.Fatalf("expired within the margin: held %v owner %q expires %v err %v", got, owner, expires, err)
80 }
81 old := time.Now().Add(-2 * lease.RenewMargin)
82 put("instances/old.example.com", old)
83 got, owner, expires, err = lease.LockState(ctx, "data", "instances/old.example.com", "test-region")
84 if err != nil || got || owner != "box-a" || !expires.Equal(old) {
85 t.Fatalf("expired past the margin: held %v owner %q expires %v err %v", got, owner, expires, err)
86 }
87}
88
89// Anything the reader cannot positively call absent or released is an
90// error, never "not held".
91func TestLockStateFailsClosed(t *testing.T) {
92 bucket := lockFixture(t)
93 ctx := context.Background()
94 bucket.Put("data", "instances/garbled.example.com/lock.json", []byte(`not json`))
95 bucket.Put("data", "instances/noexpiry.example.com/lock.json", []byte(`{"generation":1,"owner":"x"}`))
96 for _, c := range []struct{ name, bucket, location string }{
97 {"garbled body", "data", "instances/garbled.example.com"},
98 {"no expiry", "data", "instances/noexpiry.example.com"},
99 {"missing bucket", "nobucket", "instances/h.example.com"},
100 {"empty location", "data", ""},
101 {"absolute location", "data", "/instances/h.example.com"},
102 {"dot-dot location", "data", "instances/../h.example.com"},
103 {"double slash", "data", "instances//h.example.com"},
104 {"empty bucket", "", "instances/h.example.com"},
105 } {
106 got, _, _, err := lease.LockState(ctx, c.bucket, c.location, "test-region")
107 if err == nil || got {
108 t.Fatalf("%s: held %v err %v, want an error", c.name, got, err)
109 }
110 }
111}
112
113// faultLockServer answers every request with status and body, as an S3
114// endpoint (or something pretending to be one) might.
115func faultLockServer(t *testing.T, status int, body string) {
116 t.Helper()
117 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 if body != "" {
119 w.Header().Set("Content-Type", "application/xml")
120 }
121 w.WriteHeader(status)
122 io.WriteString(w, body)
123 }))
124 t.Cleanup(srv.Close)
125 lockFixture(t)
126 t.Setenv("CARLOS_S3_ENDPOINT", srv.URL)
127}
128
129// Absence is an authority-bearing answer ("the lease is free"), so only S3's
130// explicit, parseable NoSuchKey counts. Any other 404 is an error.
131func TestLockStateAbsenceNeedsAnExplicitNoSuchKey(t *testing.T) {
132 ctx := context.Background()
133 faultLockServer(t, http.StatusNotFound, `<?xml version="1.0" encoding="UTF-8"?><Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message></Error>`)
134 if got, _, _, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region"); err != nil || got {
135 t.Fatalf("explicit NoSuchKey: held %v err %v, want absent", got, err)
136 }
137 for name, c := range map[string]struct {
138 status int
139 body string
140 }{
141 "malformed 404": {http.StatusNotFound, `this is not an S3 error body`},
142 "truncated 404": {http.StatusNotFound, `<Error><Code>NoSuchKey`},
143 "empty 404": {http.StatusNotFound, ``},
144 "unrecognised 404": {http.StatusNotFound, `<Error><Code>NoSuchThing</Code><Message>x</Message></Error>`},
145 "NotFound 404": {http.StatusNotFound, `<Error><Code>NotFound</Code><Message>x</Message></Error>`},
146 "NoSuchVersion": {http.StatusNotFound, `<Error><Code>NoSuchVersion</Code><Message>x</Message></Error>`},
147 "NoSuchBucket": {http.StatusNotFound, `<Error><Code>NoSuchBucket</Code><Message>x</Message></Error>`},
148 "NoSuchKey on 200": {http.StatusOK, `<Error><Code>NoSuchKey</Code></Error>`},
149 "NoSuchKey on 403": {http.StatusForbidden, `<Error><Code>NoSuchKey</Code></Error>`},
150 } {
151 t.Run(name, func(t *testing.T) {
152 faultLockServer(t, c.status, c.body)
153 if got, _, _, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region"); err == nil || got {
154 t.Fatalf("held %v err %v, want an error", got, err)
155 }
156 })
157 }
158}
159