package objstore import ( "context" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // The four states a member is owed a different sentence for. Derived from the // blocks, never from a stored string, so this table is the whole definition. func TestEntitlementStates(t *testing.T) { now := time.Now().UTC() for _, tc := range []struct { name string e *Entitlement approved, pending, declined bool }{ {name: "never applied (nil)", e: nil}, { name: "pending", e: &Entitlement{Applied: &Applied{By: "owner@example.com", At: now}}, pending: true, }, { name: "approved", e: &Entitlement{ Applied: &Applied{By: "owner@example.com", At: now}, Decision: &Decision{Approved: true, By: "op@example.com", At: now}, }, approved: true, }, { name: "declined", e: &Entitlement{ Applied: &Applied{By: "owner@example.com", At: now}, Decision: &Decision{Approved: false, By: "op@example.com", At: now, Reason: "no"}, }, declined: true, }, } { t.Run(tc.name, func(t *testing.T) { if got := tc.e.Approved(); got != tc.approved { t.Errorf("Approved() = %v, want %v", got, tc.approved) } if got := tc.e.Pending(); got != tc.pending { t.Errorf("Pending() = %v, want %v", got, tc.pending) } if got := tc.e.Declined(); got != tc.declined { t.Errorf("Declined() = %v, want %v", got, tc.declined) } }) } } // A nil entitlement must be unapproved, not a nil dereference: every account // starts with no object at all, and the gate is consulted on every declare. func TestNilEntitlementIsFailClosed(t *testing.T) { var e *Entitlement if e.Approved() || e.Pending() || e.Declined() { t.Fatal("a nil entitlement claimed a state") } } func TestEntitlementRoundTrip(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Now().UTC().Truncate(time.Second) // Absent reads as (nil, "", nil) — Get's idiom, not an error. got, etag, err := GetEntitlement(ctx, s, "bab") if err != nil || got != nil || etag != "" { t.Fatalf("absent entitlement = %+v / %q / %v, want nil/\"\"/nil", got, etag, err) } want := &Entitlement{Applied: &Applied{By: "owner@example.com", At: now, Note: "uploads"}} if _, err := PutEntitlement(ctx, s, "bab", want, ""); err != nil { t.Fatal(err) } got, etag, err = GetEntitlement(ctx, s, "bab") if err != nil { t.Fatal(err) } if got.Schema != EntitlementSchema || got.Account != "bab" { t.Fatalf("schema/account stamped wrong: %+v", got) } if got.Applied == nil || got.Applied.By != "owner@example.com" || got.Applied.Note != "uploads" || !got.Applied.At.Equal(now) { t.Fatalf("applied = %+v", got.Applied) } if got.Decision != nil { t.Fatalf("decision = %+v, want nil for a pending application", got.Decision) } if !got.Pending() { t.Fatal("a written application did not read back as pending") } if etag == "" { t.Fatal("no etag on a written entitlement — CAS would be impossible") } } // The body must never be able to disagree with its own key. func TestPutEntitlementStampsAccountOverAnyClaim(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{Account: "bde", Schema: 99}, ""); err != nil { t.Fatal(err) } got, _, err := GetEntitlement(ctx, s, "bab") if err != nil { t.Fatal(err) } if got.Account != "bab" || got.Schema != EntitlementSchema { t.Fatalf("entitlement = %+v, want the key's account and this schema", got) } } // Two owners applying at once: the loser sees the conflict rather than // silently erasing the winner's application. func TestPutEntitlementIsCAS(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{}, ""); err != nil { t.Fatal(err) } _, stale, err := GetEntitlement(ctx, s, "bab") if err != nil { t.Fatal(err) } if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{Applied: &Applied{By: "a@example.com"}}, stale); err != nil { t.Fatal(err) } if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{Applied: &Applied{By: "b@example.com"}}, stale); err == nil { t.Fatal("a stale-etag write was accepted — concurrent applications would clobber each other") } } func TestEntitlementKeyIsOutsideTheAccountsTree(t *testing.T) { k := EntitlementKey("bab") if strings.HasPrefix(k, "accounts/") || strings.HasPrefix(k, "apps/") || strings.HasPrefix(k, "fleets/") { t.Fatalf("EntitlementKey = %q, which is inside a fleet-readable subtree "+ "(internal/edge/storedoor.go aclRead) — decline reasons would be tenant-readable", k) } if k != "store/entitlements/bab.json" { t.Fatalf("EntitlementKey = %q", k) } } func TestGetEntitlementRefusesAnInvalidAccount(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if _, _, err := GetEntitlement(ctx, s, "../../etc"); err == nil { t.Fatal("an invalid account was accepted") } if _, err := PutEntitlement(ctx, s, "../../etc", &Entitlement{}, ""); err == nil { t.Fatal("an invalid account was accepted for write") } }