carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package objstore
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12// The four states a member is owed a different sentence for. Derived from the
13// blocks, never from a stored string, so this table is the whole definition.
14func TestEntitlementStates(t *testing.T) {
15 now := time.Now().UTC()
16 for _, tc := range []struct {
17 name string
18 e *Entitlement
19 approved, pending, declined bool
20 }{
21 {name: "never applied (nil)", e: nil},
22 {
23 name: "pending",
24 e: &Entitlement{Applied: &Applied{By: "owner@example.com", At: now}},
25 pending: true,
26 },
27 {
28 name: "approved",
29 e: &Entitlement{
30 Applied: &Applied{By: "owner@example.com", At: now},
31 Decision: &Decision{Approved: true, By: "op@example.com", At: now},
32 },
33 approved: true,
34 },
35 {
36 name: "declined",
37 e: &Entitlement{
38 Applied: &Applied{By: "owner@example.com", At: now},
39 Decision: &Decision{Approved: false, By: "op@example.com", At: now, Reason: "no"},
40 },
41 declined: true,
42 },
43 } {
44 t.Run(tc.name, func(t *testing.T) {
45 if got := tc.e.Approved(); got != tc.approved {
46 t.Errorf("Approved() = %v, want %v", got, tc.approved)
47 }
48 if got := tc.e.Pending(); got != tc.pending {
49 t.Errorf("Pending() = %v, want %v", got, tc.pending)
50 }
51 if got := tc.e.Declined(); got != tc.declined {
52 t.Errorf("Declined() = %v, want %v", got, tc.declined)
53 }
54 })
55 }
56}
57
58// A nil entitlement must be unapproved, not a nil dereference: every account
59// starts with no object at all, and the gate is consulted on every declare.
60func TestNilEntitlementIsFailClosed(t *testing.T) {
61 var e *Entitlement
62 if e.Approved() || e.Pending() || e.Declined() {
63 t.Fatal("a nil entitlement claimed a state")
64 }
65}
66
67func TestEntitlementRoundTrip(t *testing.T) {
68 ctx := context.Background()
69 s := release.NewDirStore(t.TempDir())
70 now := time.Now().UTC().Truncate(time.Second)
71
72 // Absent reads as (nil, "", nil) — Get's idiom, not an error.
73 got, etag, err := GetEntitlement(ctx, s, "bab")
74 if err != nil || got != nil || etag != "" {
75 t.Fatalf("absent entitlement = %+v / %q / %v, want nil/\"\"/nil", got, etag, err)
76 }
77
78 want := &Entitlement{Applied: &Applied{By: "owner@example.com", At: now, Note: "uploads"}}
79 if _, err := PutEntitlement(ctx, s, "bab", want, ""); err != nil {
80 t.Fatal(err)
81 }
82 got, etag, err = GetEntitlement(ctx, s, "bab")
83 if err != nil {
84 t.Fatal(err)
85 }
86 if got.Schema != EntitlementSchema || got.Account != "bab" {
87 t.Fatalf("schema/account stamped wrong: %+v", got)
88 }
89 if got.Applied == nil || got.Applied.By != "owner@example.com" || got.Applied.Note != "uploads" ||
90 !got.Applied.At.Equal(now) {
91 t.Fatalf("applied = %+v", got.Applied)
92 }
93 if got.Decision != nil {
94 t.Fatalf("decision = %+v, want nil for a pending application", got.Decision)
95 }
96 if !got.Pending() {
97 t.Fatal("a written application did not read back as pending")
98 }
99 if etag == "" {
100 t.Fatal("no etag on a written entitlement — CAS would be impossible")
101 }
102}
103
104// The body must never be able to disagree with its own key.
105func TestPutEntitlementStampsAccountOverAnyClaim(t *testing.T) {
106 ctx := context.Background()
107 s := release.NewDirStore(t.TempDir())
108 if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{Account: "bde", Schema: 99}, ""); err != nil {
109 t.Fatal(err)
110 }
111 got, _, err := GetEntitlement(ctx, s, "bab")
112 if err != nil {
113 t.Fatal(err)
114 }
115 if got.Account != "bab" || got.Schema != EntitlementSchema {
116 t.Fatalf("entitlement = %+v, want the key's account and this schema", got)
117 }
118}
119
120// Two owners applying at once: the loser sees the conflict rather than
121// silently erasing the winner's application.
122func TestPutEntitlementIsCAS(t *testing.T) {
123 ctx := context.Background()
124 s := release.NewDirStore(t.TempDir())
125 if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{}, ""); err != nil {
126 t.Fatal(err)
127 }
128 _, stale, err := GetEntitlement(ctx, s, "bab")
129 if err != nil {
130 t.Fatal(err)
131 }
132 if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{Applied: &Applied{By: "a@example.com"}}, stale); err != nil {
133 t.Fatal(err)
134 }
135 if _, err := PutEntitlement(ctx, s, "bab", &Entitlement{Applied: &Applied{By: "b@example.com"}}, stale); err == nil {
136 t.Fatal("a stale-etag write was accepted — concurrent applications would clobber each other")
137 }
138}
139
140func TestEntitlementKeyIsOutsideTheAccountsTree(t *testing.T) {
141 k := EntitlementKey("bab")
142 if strings.HasPrefix(k, "accounts/") || strings.HasPrefix(k, "apps/") || strings.HasPrefix(k, "fleets/") {
143 t.Fatalf("EntitlementKey = %q, which is inside a fleet-readable subtree "+
144 "(internal/edge/storedoor.go aclRead) — decline reasons would be tenant-readable", k)
145 }
146 if k != "store/entitlements/bab.json" {
147 t.Fatalf("EntitlementKey = %q", k)
148 }
149}
150
151func TestGetEntitlementRefusesAnInvalidAccount(t *testing.T) {
152 ctx := context.Background()
153 s := release.NewDirStore(t.TempDir())
154 if _, _, err := GetEntitlement(ctx, s, "../../etc"); err == nil {
155 t.Fatal("an invalid account was accepted")
156 }
157 if _, err := PutEntitlement(ctx, s, "../../etc", &Entitlement{}, ""); err == nil {
158 t.Fatal("an invalid account was accepted for write")
159 }
160}
161