package lease_test import ( "context" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/lease/leasetest" ) // lockFixture points the package's S3 clients at leasetest's local store, // with credentials the AWS chain resolves without touching the machine's. func lockFixture(t *testing.T) *leasetest.VersionedBucket { t.Helper() bucket := leasetest.NewVersionedBucket() bucket.AddBucket("data") handler, _ := leasetest.NewS3Server(bucket) srv := httptest.NewServer(handler) t.Cleanup(srv.Close) t.Setenv("AWS_ACCESS_KEY_ID", "test-key") t.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret") t.Setenv("AWS_SESSION_TOKEN", "") t.Setenv("AWS_EC2_METADATA_DISABLED", "true") t.Setenv("AWS_CONFIG_FILE", t.TempDir()+"/absent") t.Setenv("AWS_SHARED_CREDENTIALS_FILE", t.TempDir()+"/absent") t.Setenv("CARLOS_S3_ENDPOINT", srv.URL) return bucket } func TestLockStateFollowsAcquireAndRelease(t *testing.T) { lockFixture(t) ctx := context.Background() held, err := lease.Acquire(ctx, "data", "instances/h.example.com", "test-region") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = held.Release(context.Background()) }) got, _, expires, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region") if err != nil || !got || !expires.After(time.Now()) { t.Fatalf("after acquire: held %v expires %v err %v, want held with an expiry in the future", got, expires, err) } // ReplicaSource's prefix carries a trailing slash; the lock is the same. if got, _, _, err := lease.LockState(ctx, "data", "instances/h.example.com/", "test-region"); err != nil || !got { t.Fatalf("trailing slash: held %v err %v", got, err) } if err := held.Release(ctx); err != nil { t.Fatal(err) } if got, owner, expires, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region"); err != nil || got || owner != "" || !expires.IsZero() { t.Fatalf("after release: held %v owner %q expires %v err %v", got, owner, expires, err) } } func TestLockStateAbsentPrefixIsNotHeld(t *testing.T) { lockFixture(t) got, owner, expires, err := lease.LockState(context.Background(), "data", "instances/never.example.com/inc-1", "test-region") if err != nil || got || owner != "" || !expires.IsZero() { t.Fatalf("absent: held %v owner %q expires %v err %v", got, owner, expires, err) } } // ยง5.3 step 4: a lock counts as released only once it has expired by more // than the lease's renewal margin, so a holder whose clock runs a little // behind is still counted as holding. func TestLockStateExpiredWithinTheMarginIsHeld(t *testing.T) { bucket := lockFixture(t) ctx := context.Background() put := func(loc string, expires time.Time) { bucket.Put("data", loc+"/lock.json", []byte(`{"generation":3,"expires_at":"`+expires.UTC().Format(time.RFC3339Nano)+`","owner":"box-a"}`)) } recent := time.Now().Add(-lease.RenewMargin / 2) put("instances/recent.example.com", recent) got, owner, expires, err := lease.LockState(ctx, "data", "instances/recent.example.com", "test-region") if err != nil || !got || owner != "box-a" || !expires.Equal(recent) { t.Fatalf("expired within the margin: held %v owner %q expires %v err %v", got, owner, expires, err) } old := time.Now().Add(-2 * lease.RenewMargin) put("instances/old.example.com", old) got, owner, expires, err = lease.LockState(ctx, "data", "instances/old.example.com", "test-region") if err != nil || got || owner != "box-a" || !expires.Equal(old) { t.Fatalf("expired past the margin: held %v owner %q expires %v err %v", got, owner, expires, err) } } // Anything the reader cannot positively call absent or released is an // error, never "not held". func TestLockStateFailsClosed(t *testing.T) { bucket := lockFixture(t) ctx := context.Background() bucket.Put("data", "instances/garbled.example.com/lock.json", []byte(`not json`)) bucket.Put("data", "instances/noexpiry.example.com/lock.json", []byte(`{"generation":1,"owner":"x"}`)) for _, c := range []struct{ name, bucket, location string }{ {"garbled body", "data", "instances/garbled.example.com"}, {"no expiry", "data", "instances/noexpiry.example.com"}, {"missing bucket", "nobucket", "instances/h.example.com"}, {"empty location", "data", ""}, {"absolute location", "data", "/instances/h.example.com"}, {"dot-dot location", "data", "instances/../h.example.com"}, {"double slash", "data", "instances//h.example.com"}, {"empty bucket", "", "instances/h.example.com"}, } { got, _, _, err := lease.LockState(ctx, c.bucket, c.location, "test-region") if err == nil || got { t.Fatalf("%s: held %v err %v, want an error", c.name, got, err) } } } // faultLockServer answers every request with status and body, as an S3 // endpoint (or something pretending to be one) might. func faultLockServer(t *testing.T, status int, body string) { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if body != "" { w.Header().Set("Content-Type", "application/xml") } w.WriteHeader(status) io.WriteString(w, body) })) t.Cleanup(srv.Close) lockFixture(t) t.Setenv("CARLOS_S3_ENDPOINT", srv.URL) } // Absence is an authority-bearing answer ("the lease is free"), so only S3's // explicit, parseable NoSuchKey counts. Any other 404 is an error. func TestLockStateAbsenceNeedsAnExplicitNoSuchKey(t *testing.T) { ctx := context.Background() faultLockServer(t, http.StatusNotFound, `NoSuchKeyThe specified key does not exist.`) if got, _, _, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region"); err != nil || got { t.Fatalf("explicit NoSuchKey: held %v err %v, want absent", got, err) } for name, c := range map[string]struct { status int body string }{ "malformed 404": {http.StatusNotFound, `this is not an S3 error body`}, "truncated 404": {http.StatusNotFound, `NoSuchKey`}, "empty 404": {http.StatusNotFound, ``}, "unrecognised 404": {http.StatusNotFound, `NoSuchThingx`}, "NotFound 404": {http.StatusNotFound, `NotFoundx`}, "NoSuchVersion": {http.StatusNotFound, `NoSuchVersionx`}, "NoSuchBucket": {http.StatusNotFound, `NoSuchBucketx`}, "NoSuchKey on 200": {http.StatusOK, `NoSuchKey`}, "NoSuchKey on 403": {http.StatusForbidden, `NoSuchKey`}, } { t.Run(name, func(t *testing.T) { faultLockServer(t, c.status, c.body) if got, _, _, err := lease.LockState(ctx, "data", "instances/h.example.com", "test-region"); err == nil || got { t.Fatalf("held %v err %v, want an error", got, err) } }) } }