oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import "testing"
4
5// The two-user isolation suite AGENTS.md requires.
6//
7// Every test here asks the same question from a different angle: can
8// somebody reach a sheet nobody gave them? The answer must always be
9// "they get exactly what a nonexistent sheet gives" — a 404, never a
10// 403, because a 403 confirms the row exists and the set of ids that
11// 403 is a map of the team's sheets.
12
13// guestOn shares a sheet with a fresh address at a role, and returns
14// that member. The address is deliberately outside SHEETS_OWNERS, so
15// the only way they reach anything is the grant itself.
16func guestOn(t *testing.T, app *App, sheetID uint64, address string, role SheetRole) Member {
17 t.Helper()
18 m, err := app.grant(sheetID, address, role, signedInMember(t, app))
19 if err != nil {
20 t.Fatalf("grant %s as %s: %v", address, role, err)
21 }
22 if TeamRole(m.TeamRole) != TeamGuest {
23 t.Fatalf("%s was minted as %q, want guest — a guest wrongly made a member "+
24 "sees every sheet on the instance", address, m.TeamRole)
25 }
26 return m
27}
28
29func TestAGuestSeesOnlyWhatTheyWereGranted(t *testing.T) {
30 app, cookies := signIn(t)
31 shared, _ := newSheet(t, app, cookies, "Shared with Ada")
32 private, _ := newSheet(t, app, cookies, "Not shared")
33
34 ada := guestOn(t, app, shared.ID, "ada@elsewhere.example", SheetViewer)
35
36 got, err := app.listSheets(ada)
37 if err != nil {
38 t.Fatalf("listSheets: %v", err)
39 }
40 if len(got) != 1 {
41 t.Fatalf("a guest granted one sheet sees %d: %+v", len(got), got)
42 }
43 if got[0].ID != shared.ID {
44 t.Fatalf("the guest sees sheet %d, want the one they were granted (%d)", got[0].ID, shared.ID)
45 }
46 if app.mayRead(private.ID, ada) {
47 t.Fatal("a guest can read a sheet nobody granted them")
48 }
49}
50
51func TestAGuestWithNoGrantsSeesNothing(t *testing.T) {
52 app, cookies := signIn(t)
53 newSheet(t, app, cookies, "Team sheet")
54
55 stranger, err := app.memberFor("nobody@elsewhere.example")
56 if err != nil {
57 t.Fatalf("memberFor: %v", err)
58 }
59 got, err := app.listSheets(stranger)
60 if err != nil {
61 t.Fatalf("listSheets: %v", err)
62 }
63 // Empty, not everything. The inner join is what makes this true;
64 // a left join here would hand a stranger the whole instance.
65 if len(got) != 0 {
66 t.Fatalf("a member with no grants sees %d sheets, want 0", len(got))
67 }
68}
69
70// A colleague reaches every sheet — that is what one instance per team
71// means, and it is the half the guest rule must not break.
72func TestATeamMemberSeesEverySheet(t *testing.T) {
73 app, cookies := signIn(t)
74 one, _ := newSheet(t, app, cookies, "One")
75 two, _ := newSheet(t, app, cookies, "Two")
76
77 colleague, err := app.memberFor("colleague@example.com")
78 if err != nil {
79 t.Fatalf("memberFor: %v", err)
80 }
81 if err := app.db.Model(&colleague).Update("team_role", string(TeamMember)).Error; err != nil {
82 t.Fatalf("promote: %v", err)
83 }
84 colleague.TeamRole = string(TeamMember)
85
86 got, err := app.listSheets(colleague)
87 if err != nil {
88 t.Fatalf("listSheets: %v", err)
89 }
90 if len(got) != 2 {
91 t.Fatalf("a team member sees %d of 2 sheets", len(got))
92 }
93 if !app.mayWrite(one.ID, colleague) || !app.mayWrite(two.ID, colleague) {
94 t.Fatal("a team member cannot write to a team sheet")
95 }
96 // But not owner: sharing is not something every colleague inherits.
97 if app.mayShare(one.ID, colleague) {
98 t.Fatal("a team member can change who else can read a sheet they were never given")
99 }
100}
101
102func TestRolesAreOrdered(t *testing.T) {
103 cases := []struct {
104 have, want SheetRole
105 allowed bool
106 }{
107 {SheetOwner, SheetEditor, true},
108 {SheetOwner, SheetViewer, true},
109 {SheetEditor, SheetEditor, true},
110 {SheetEditor, SheetOwner, false},
111 {SheetCommenter, SheetViewer, true},
112 {SheetCommenter, SheetEditor, false},
113 {SheetViewer, SheetViewer, true},
114 {SheetViewer, SheetCommenter, false},
115 // An unknown role ranks below every real one, so a row written
116 // by a newer build grants nothing rather than everything.
117 {SheetRole("admin"), SheetViewer, false},
118 {SheetRole(""), SheetViewer, false},
119 }
120 for _, c := range cases {
121 if got := c.have.Allows(c.want); got != c.allowed {
122 t.Errorf("%q.Allows(%q) = %v, want %v", c.have, c.want, got, c.allowed)
123 }
124 }
125}
126
127func TestRevokeRemovesAccess(t *testing.T) {
128 app, cookies := signIn(t)
129 s, _ := newSheet(t, app, cookies, "Shared then not")
130 ada := guestOn(t, app, s.ID, "ada@elsewhere.example", SheetViewer)
131
132 if !app.mayRead(s.ID, ada) {
133 t.Fatal("the grant did not take")
134 }
135 if err := app.revoke(s.ID, ada.ID); err != nil {
136 t.Fatalf("revoke: %v", err)
137 }
138 if app.mayRead(s.ID, ada) {
139 t.Fatal("a revoked guest can still read")
140 }
141 // Revoking again is not an error: the caller wanted them not to
142 // have access, and they do not.
143 if err := app.revoke(s.ID, ada.ID); err != nil {
144 t.Fatalf("revoking twice: %v", err)
145 }
146}
147
148// Re-sharing changes the role rather than failing on the unique index,
149// which is what somebody doing it expects.
150func TestRegrantingChangesTheRole(t *testing.T) {
151 app, cookies := signIn(t)
152 s, _ := newSheet(t, app, cookies, "Promote me")
153 ada := guestOn(t, app, s.ID, "ada@elsewhere.example", SheetViewer)
154
155 if _, err := app.grant(s.ID, "ada@elsewhere.example", SheetEditor, signedInMember(t, app)); err != nil {
156 t.Fatalf("re-grant: %v", err)
157 }
158 role, ok := app.roleFor(s.ID, ada)
159 if !ok || role != SheetEditor {
160 t.Fatalf("after re-granting, role = %q (%v), want editor", role, ok)
161 }
162}
163
164func TestGrantRefusesRubbish(t *testing.T) {
165 app, cookies := signIn(t)
166 s, _ := newSheet(t, app, cookies, "Sheet")
167 by := signedInMember(t, app)
168
169 for _, tc := range []struct {
170 address string
171 role SheetRole
172 }{
173 {"", SheetViewer},
174 {" ", SheetViewer},
175 {"not-an-address", SheetViewer},
176 {"ada@elsewhere.example", SheetRole("admin")},
177 {"ada@elsewhere.example", SheetRole("")},
178 } {
179 if _, err := app.grant(s.ID, tc.address, tc.role, by); err == nil {
180 t.Errorf("grant(%q, %q) was accepted", tc.address, tc.role)
181 }
182 }
183}
184
185// The creator owns what they made, written as a Grant rather than
186// inferred from Sheet.CreatedBy — two sources of truth for one question
187// is how the share screen ends up unable to hand ownership on.
188func TestTheCreatorOwnsTheSheet(t *testing.T) {
189 app, cookies := signIn(t)
190 s, _ := newSheet(t, app, cookies, "Mine")
191 me := signedInMember(t, app)
192
193 role, ok := app.roleFor(s.ID, me)
194 if !ok || role != SheetOwner {
195 t.Fatalf("the creator's role is %q (%v), want owner", role, ok)
196 }
197 if !app.mayShare(s.ID, me) {
198 t.Fatal("the creator cannot share their own sheet")
199 }
200}
201
202// Sharing has to make somebody able to sign in, or the grant is a row
203// nobody can ever exercise. This is what the app did between the grants
204// table landing and App.Admits existing.
205func TestASharedWithGuestMaySignIn(t *testing.T) {
206 app, cookies := signIn(t)
207 s, _ := newSheet(t, app, cookies, "Shared")
208
209 if app.Admits("ada@elsewhere.example") {
210 t.Fatal("a stranger is admitted before any share")
211 }
212 guestOn(t, app, s.ID, "ada@elsewhere.example", SheetViewer)
213 if !app.Admits("ada@elsewhere.example") {
214 t.Fatal("a guest who holds a grant is refused sign-in; the share would be unusable")
215 }
216 // Case and padding must not decide who gets in.
217 if !app.Admits(" Ada@Elsewhere.Example ") {
218 t.Fatal("admission is case-sensitive; the same person would be refused")
219 }
220}
221
222func TestAdmissionIsFailClosed(t *testing.T) {
223 app, _ := signIn(t)
224 for _, addr := range []string{"", " ", "nobody@elsewhere.example"} {
225 if app.Admits(addr) {
226 t.Errorf("Admits(%q) = true", addr)
227 }
228 }
229}
230
231// A grant row whose role this build cannot read must grant nothing, not
232// fall through to team standing — falling through would silently
233// upgrade an unreadable grant to full access.
234func TestAnUnreadableGrantGrantsNothing(t *testing.T) {
235 app, cookies := signIn(t)
236 s, _ := newSheet(t, app, cookies, "Sheet")
237 me := signedInMember(t, app)
238
239 if err := app.db.Model(&Grant{}).
240 Where("sheet_id = ? AND member_id = ?", s.ID, me.ID).
241 Update("role", "from-the-future").Error; err != nil {
242 t.Fatalf("corrupt the grant: %v", err)
243 }
244 if _, ok := app.roleFor(s.ID, me); ok {
245 t.Fatal("a grant with an unrecognised role still granted access")
246 }
247}
248