@marktiderman/genesis-supabase 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +377 -23
- package/dist/blocks.d.ts +19 -0
- package/dist/blocks.js +3 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chunk-FAA2FTF4.js +110 -0
- package/dist/chunk-FAA2FTF4.js.map +1 -0
- package/dist/chunk-T2HGFC4C.js +37 -0
- package/dist/chunk-T2HGFC4C.js.map +1 -0
- package/dist/index-BSGz3RGk.d.ts +317 -0
- package/dist/index.d.ts +4 -103
- package/dist/index.js +60 -3
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +90 -6
- package/dist/native.js +70 -2
- package/dist/native.js.map +1 -1
- package/functions/genesis-apple-revoke/apple.ts +237 -0
- package/functions/genesis-apple-revoke/handler.ts +227 -0
- package/functions/genesis-apple-revoke/index.ts +55 -0
- package/functions/genesis-push-send/expo.ts +117 -0
- package/functions/genesis-push-send/handler.ts +462 -0
- package/functions/genesis-push-send/index.ts +54 -0
- package/functions/genesis-support-cleanup/handler.ts +80 -0
- package/functions/genesis-support-cleanup/index.ts +39 -0
- package/functions/genesis-support-intake/contract.ts +193 -0
- package/functions/genesis-support-intake/handler.ts +170 -0
- package/functions/genesis-support-intake/index.ts +47 -0
- package/functions/genesis-support-notify/handler.ts +167 -0
- package/functions/genesis-support-notify/index.ts +15 -0
- package/functions/tests/genesis-apple-revoke_test.ts +81 -0
- package/functions/tests/genesis-push-send_test.ts +53 -0
- package/migrations/0003_genesis_apple_token_revocation.sql +205 -0
- package/migrations/0004_genesis_tenant_features.sql +333 -0
- package/migrations/0005_genesis_push.sql +524 -0
- package/migrations/0006_genesis_support.sql +846 -0
- package/migrations/tests/0003_genesis_apple_token_revocation.test.sql +230 -0
- package/migrations/tests/0004_genesis_tenant_features.test.sql +418 -0
- package/migrations/tests/0005_genesis_push.test.sql +407 -0
- package/migrations/tests/0006_genesis_support.test.sql +552 -0
- package/package.json +91 -4
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
-- @feat:support-feedback @feat:feature-voting
|
|
2
|
+
-- pgTAP test for 0006_genesis_support.sql.
|
|
3
|
+
-- Copy into your app's `supabase/tests/database/` and run `supabase test db`.
|
|
4
|
+
-- It writes only its own users, reports, ideas and storage rows and rolls everything back.
|
|
5
|
+
-- If your app already has the staff hook (`private.genesis_support_staff`), it is dropped
|
|
6
|
+
-- and replaced inside this transaction only.
|
|
7
|
+
--
|
|
8
|
+
-- Signing in: auth.uid() reads request.jwt.claims (and the legacy
|
|
9
|
+
-- request.jwt.claim.sub on older images), so pg_temp.sign_in sets both.
|
|
10
|
+
begin;
|
|
11
|
+
create extension if not exists pgtap with schema extensions;
|
|
12
|
+
select plan(119);
|
|
13
|
+
|
|
14
|
+
-- People: a1 reports, b2 is someone else, c3 is staff (once the hook exists),
|
|
15
|
+
-- d4 is a Supabase anonymous sign-in, e5 never votes.
|
|
16
|
+
insert into auth.users (id, aud, role, email, instance_id, is_anonymous)
|
|
17
|
+
select ('00000000-0000-4000-8000-0000000000' || n)::uuid, 'authenticated', 'authenticated',
|
|
18
|
+
case when n = 'd4' then null else 'genesis-support-' || n || '@example.test' end,
|
|
19
|
+
'00000000-0000-0000-0000-000000000000', n = 'd4'
|
|
20
|
+
from unnest(array['a1', 'b2', 'c3', 'd4', 'e5']) n;
|
|
21
|
+
|
|
22
|
+
create function pg_temp.person(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-0000000000' || n)::uuid $$;
|
|
23
|
+
create function pg_temp.sign_in(n text) returns void language plpgsql as $$
|
|
24
|
+
begin
|
|
25
|
+
perform set_config('request.jwt.claims',
|
|
26
|
+
json_build_object('sub', pg_temp.person(n), 'role', 'authenticated', 'is_anonymous', n = 'd4')::text, true);
|
|
27
|
+
perform set_config('request.jwt.claim.sub', pg_temp.person(n)::text, true);
|
|
28
|
+
end $$;
|
|
29
|
+
|
|
30
|
+
-- Ids the test hands between roles, and the rows a statement touched.
|
|
31
|
+
create table pg_temp.ids (name text primary key, id uuid);
|
|
32
|
+
create function pg_temp.remember(name text, q text) returns void language plpgsql as $$
|
|
33
|
+
declare v uuid;
|
|
34
|
+
begin
|
|
35
|
+
execute q into v;
|
|
36
|
+
insert into pg_temp.ids values (name, v);
|
|
37
|
+
end $$;
|
|
38
|
+
create function pg_temp.affected(q text) returns int language plpgsql as $$
|
|
39
|
+
declare n int;
|
|
40
|
+
begin
|
|
41
|
+
execute q;
|
|
42
|
+
get diagnostics n = row_count;
|
|
43
|
+
return n;
|
|
44
|
+
end $$;
|
|
45
|
+
create function pg_temp.id(n text) returns uuid language sql stable as $$ select id from pg_temp.ids where name = n $$;
|
|
46
|
+
grant select, insert on pg_temp.ids to anon, authenticated;
|
|
47
|
+
grant execute on function pg_temp.person(text), pg_temp.sign_in(text), pg_temp.remember(text, text), pg_temp.affected(text), pg_temp.id(text)
|
|
48
|
+
to anon, authenticated, service_role;
|
|
49
|
+
|
|
50
|
+
drop function if exists private.genesis_support_staff(uuid);
|
|
51
|
+
|
|
52
|
+
-- Shape -------------------------------------------------------------------------
|
|
53
|
+
select has_table('public', 'support_reports', 'support_reports exists');
|
|
54
|
+
select has_table('public', 'support_attachments', 'support_attachments exists');
|
|
55
|
+
select has_table('public', 'support_replies', 'support_replies exists');
|
|
56
|
+
select has_table('public', 'support_ideas', 'support_ideas exists');
|
|
57
|
+
select has_table('public', 'support_idea_votes', 'support_idea_votes exists');
|
|
58
|
+
select col_is_pk('public', 'support_idea_votes', array['idea_id', 'user_id'], 'one vote per person per idea');
|
|
59
|
+
select ok(
|
|
60
|
+
(select bool_and(relrowsecurity) from pg_class where oid in (
|
|
61
|
+
'public.support_reports'::regclass, 'public.support_attachments'::regclass, 'public.support_attachment_purges'::regclass,
|
|
62
|
+
'public.support_replies'::regclass, 'public.support_ideas'::regclass, 'public.support_idea_votes'::regclass)),
|
|
63
|
+
'RLS is on for every support table'
|
|
64
|
+
);
|
|
65
|
+
select is(
|
|
66
|
+
(select count(*)::int from pg_policy where polrelid = 'public.support_attachment_purges'::regclass),
|
|
67
|
+
0,
|
|
68
|
+
'the purge queue has no policies: the service role only'
|
|
69
|
+
);
|
|
70
|
+
select ok(
|
|
71
|
+
(select not public and file_size_limit = 5242880 and allowed_mime_types = array['image/png', 'image/jpeg', 'image/webp']
|
|
72
|
+
from storage.buckets where id = 'genesis-support'),
|
|
73
|
+
'the genesis-support bucket is private, images only, 5 MB each'
|
|
74
|
+
);
|
|
75
|
+
select is(
|
|
76
|
+
(select count(*)::int from pg_policy where polrelid = 'storage.objects'::regclass and polname like 'genesis_support_%' and polcmd in ('w', 'd', '*') and polpermissive),
|
|
77
|
+
0,
|
|
78
|
+
'no permissive update or delete policy on the bucket: nothing is overwritten or removed by a person'
|
|
79
|
+
);
|
|
80
|
+
select is(
|
|
81
|
+
(select count(*)::int from pg_policy where polrelid = 'storage.objects'::regclass and polname like 'genesis_support_%' and not polpermissive),
|
|
82
|
+
4,
|
|
83
|
+
'restrictive policies hold the bucket to these rules whatever else the app allows'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
-- Privileges ------------------------------------------------------------------------
|
|
87
|
+
select ok(
|
|
88
|
+
not has_table_privilege('anon', 'public.support_reports', 'select,insert,update,delete')
|
|
89
|
+
and not has_table_privilege('anon', 'public.support_replies', 'select,insert,update,delete')
|
|
90
|
+
and not has_table_privilege('anon', 'public.support_attachments', 'select,insert,update,delete')
|
|
91
|
+
and not has_table_privilege('anon', 'public.support_ideas', 'select,insert,update,delete')
|
|
92
|
+
and not has_table_privilege('anon', 'public.support_idea_votes', 'select,insert,update,delete'),
|
|
93
|
+
'anon has no privilege on any support table'
|
|
94
|
+
);
|
|
95
|
+
select ok(
|
|
96
|
+
not has_table_privilege('authenticated', 'public.support_attachment_purges', 'select,insert,update,delete'),
|
|
97
|
+
'authenticated cannot reach the purge queue'
|
|
98
|
+
);
|
|
99
|
+
select ok(
|
|
100
|
+
not has_column_privilege('authenticated', 'public.support_reports', 'status', 'insert')
|
|
101
|
+
and not has_column_privilege('authenticated', 'public.support_reports', 'status', 'update')
|
|
102
|
+
and not has_column_privilege('authenticated', 'public.support_reports', 'user_id', 'insert')
|
|
103
|
+
and not has_column_privilege('authenticated', 'public.support_reports', 'feature_id', 'update')
|
|
104
|
+
and not has_column_privilege('authenticated', 'public.support_reports', 'staff_read_at', 'update'),
|
|
105
|
+
'a person cannot set a report''s status, owner, feature or read receipts'
|
|
106
|
+
);
|
|
107
|
+
select ok(
|
|
108
|
+
not has_column_privilege('authenticated', 'public.support_ideas', 'status', 'update')
|
|
109
|
+
and not has_column_privilege('authenticated', 'public.support_ideas', 'vote_count', 'update')
|
|
110
|
+
and not has_column_privilege('authenticated', 'public.support_ideas', 'vote_count', 'insert')
|
|
111
|
+
and not has_column_privilege('authenticated', 'public.support_idea_votes', 'user_id', 'insert')
|
|
112
|
+
and not has_table_privilege('authenticated', 'public.support_idea_votes', 'update'),
|
|
113
|
+
'nobody signed in sets an idea''s status or count, or votes as someone else'
|
|
114
|
+
);
|
|
115
|
+
select ok(
|
|
116
|
+
not has_column_privilege('authenticated', 'public.support_ideas', 'author_id', 'select')
|
|
117
|
+
and not has_column_privilege('authenticated', 'public.support_replies', 'author_id', 'select'),
|
|
118
|
+
'nobody reads who suggested an idea or which staff member replied'
|
|
119
|
+
);
|
|
120
|
+
select ok(
|
|
121
|
+
not has_column_privilege('authenticated', 'public.support_replies', 'from_staff', 'insert')
|
|
122
|
+
and not has_column_privilege('authenticated', 'public.support_replies', 'author_id', 'insert')
|
|
123
|
+
and not has_table_privilege('authenticated', 'public.support_replies', 'update,delete'),
|
|
124
|
+
'a reply''s author and staff flag are the server''s, and replies are never edited'
|
|
125
|
+
);
|
|
126
|
+
select ok(
|
|
127
|
+
not has_function_privilege('anon', 'public.support_is_staff()', 'execute')
|
|
128
|
+
and not has_function_privilege('anon', 'public.support_mark_read(uuid)', 'execute')
|
|
129
|
+
and not has_function_privilege('anon', 'public.support_set_report_status(uuid, text, text)', 'execute')
|
|
130
|
+
and not has_function_privilege('anon', 'public.support_set_idea_status(uuid, text, text)', 'execute')
|
|
131
|
+
and not has_function_privilege('anon', 'public.support_shipped_for_me(integer)', 'execute'),
|
|
132
|
+
'anon can execute none of the functions'
|
|
133
|
+
);
|
|
134
|
+
select ok(
|
|
135
|
+
not has_function_privilege('authenticated', 'public.support_orphan_objects(interval, integer)', 'execute')
|
|
136
|
+
and has_function_privilege('service_role', 'public.support_orphan_objects(interval, integer)', 'execute'),
|
|
137
|
+
'only the service role lists orphaned objects'
|
|
138
|
+
);
|
|
139
|
+
select ok(
|
|
140
|
+
not exists (
|
|
141
|
+
select 1 from pg_proc p, aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) a
|
|
142
|
+
where p.pronamespace = 'private'::regnamespace and p.proname like 'genesis_support_%'
|
|
143
|
+
and a.grantee in (0, 'anon'::regrole, 'authenticated'::regrole) and a.privilege_type = 'EXECUTE'
|
|
144
|
+
),
|
|
145
|
+
'public, anon and authenticated execute no private support function'
|
|
146
|
+
);
|
|
147
|
+
select ok(
|
|
148
|
+
(select bool_and(not p.prosecdef or p.proconfig @> array['search_path=""']) from pg_proc p
|
|
149
|
+
where p.proname like 'support_%' or p.proname like 'genesis_support_%'),
|
|
150
|
+
'every security definer function pins search_path'
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
-- Reports -----------------------------------------------------------------------------
|
|
154
|
+
select pg_temp.sign_in('a1');
|
|
155
|
+
set local role authenticated;
|
|
156
|
+
select pg_temp.remember('r1', $$
|
|
157
|
+
insert into public.support_reports (type, message, route, app_version, platform, device, context)
|
|
158
|
+
values ('bug', 'The save button does nothing', '/settings/profile', '1.4.0 (37) · preview · 3f2a9c1b', 'ios', '{"model": "iPhone"}', '{}')
|
|
159
|
+
returning id $$);
|
|
160
|
+
select is((select status from public.support_reports where id = pg_temp.id('r1')), 'open', 'a person sends a report, and it starts open');
|
|
161
|
+
select throws_ok(
|
|
162
|
+
$$ insert into public.support_reports (message, status) values ('x', 'addressed') $$,
|
|
163
|
+
'42501', null, 'a person cannot send a report with a status'
|
|
164
|
+
);
|
|
165
|
+
select throws_ok(
|
|
166
|
+
$$ insert into public.support_reports (message, route) values ('x', '/reset?token=abc') $$,
|
|
167
|
+
'23514', null, 'a route with a query string is refused'
|
|
168
|
+
);
|
|
169
|
+
select throws_ok(
|
|
170
|
+
$$ insert into public.support_reports (message, device) values ('x', jsonb_build_object('blob', repeat(md5(random()::text), 300))) $$,
|
|
171
|
+
'23514', null, 'device info over 4 KB is refused'
|
|
172
|
+
);
|
|
173
|
+
select is(
|
|
174
|
+
pg_temp.affected($q$ update public.support_reports set message = 'The save button does nothing on iOS' where id = pg_temp.id('r1') $q$),
|
|
175
|
+
1,
|
|
176
|
+
'a person edits their open report'
|
|
177
|
+
);
|
|
178
|
+
select throws_ok(
|
|
179
|
+
$$ update public.support_reports set status = 'addressed' $$,
|
|
180
|
+
'42501', null, 'a person cannot change a status'
|
|
181
|
+
);
|
|
182
|
+
reset role;
|
|
183
|
+
|
|
184
|
+
select pg_temp.sign_in('b2');
|
|
185
|
+
set local role authenticated;
|
|
186
|
+
select is((select count(*)::int from public.support_reports), 0, 'someone else sees no one else''s report');
|
|
187
|
+
select is(pg_temp.affected($q$ update public.support_reports set message = 'mine now' $q$), 0, '... edits none');
|
|
188
|
+
select is(pg_temp.affected($q$ delete from public.support_reports $q$), 0, '... and deletes none');
|
|
189
|
+
select is(public.support_is_staff(), false, 'without the hook nobody is staff');
|
|
190
|
+
reset role;
|
|
191
|
+
|
|
192
|
+
select pg_temp.sign_in('c3');
|
|
193
|
+
set local role authenticated;
|
|
194
|
+
select is((select count(*)::int from public.support_reports), 0, 'staff see nothing before the app''s hook exists');
|
|
195
|
+
select throws_ok(
|
|
196
|
+
$$ select public.support_set_report_status(pg_temp.id('r1'), 'in_progress') $$,
|
|
197
|
+
'42501', null, '... and change nothing'
|
|
198
|
+
);
|
|
199
|
+
reset role;
|
|
200
|
+
|
|
201
|
+
-- The app's hook: c3 is staff.
|
|
202
|
+
create function private.genesis_support_staff(person uuid) returns boolean
|
|
203
|
+
language sql stable set search_path = '' as $$ select person = '00000000-0000-4000-8000-0000000000c3'::uuid $$;
|
|
204
|
+
revoke execute on function private.genesis_support_staff(uuid) from public, anon, authenticated;
|
|
205
|
+
|
|
206
|
+
select pg_temp.sign_in('c3');
|
|
207
|
+
set local role authenticated;
|
|
208
|
+
select is(public.support_is_staff(), true, 'the hook makes c3 staff');
|
|
209
|
+
select is((select count(*)::int from public.support_reports), 1, 'staff read every report');
|
|
210
|
+
select lives_ok(
|
|
211
|
+
$$ select public.support_set_report_status(pg_temp.id('r1'), 'in_progress', 'support-feedback') $$,
|
|
212
|
+
'staff set a report''s status and feature'
|
|
213
|
+
);
|
|
214
|
+
select lives_ok($$ select public.support_set_report_status(pg_temp.id('r1'), 'in_progress') $$, 'a status change without a feature');
|
|
215
|
+
select is((select feature_id from public.support_reports where id = pg_temp.id('r1')), 'support-feedback', '... keeps the feature');
|
|
216
|
+
select throws_ok(
|
|
217
|
+
$$ select public.support_set_report_status(pg_temp.id('r1'), 'done') $$,
|
|
218
|
+
'23514', null, 'an unknown status is refused'
|
|
219
|
+
);
|
|
220
|
+
select is(pg_temp.affected($q$ update public.support_reports set message = 'edited by staff' $q$), 0, 'staff don''t edit a person''s words');
|
|
221
|
+
reset role;
|
|
222
|
+
|
|
223
|
+
select pg_temp.sign_in('b2');
|
|
224
|
+
set local role authenticated;
|
|
225
|
+
select is(public.support_is_staff(), false, 'the hook answers only for its staff');
|
|
226
|
+
select throws_ok(
|
|
227
|
+
$$ select public.support_set_report_status(pg_temp.id('r1'), 'wont_fix') $$,
|
|
228
|
+
'42501', null, 'someone else cannot set a status'
|
|
229
|
+
);
|
|
230
|
+
reset role;
|
|
231
|
+
|
|
232
|
+
select pg_temp.sign_in('a1');
|
|
233
|
+
set local role authenticated;
|
|
234
|
+
select is(
|
|
235
|
+
(select status || ':' || feature_id from public.support_reports where id = pg_temp.id('r1')),
|
|
236
|
+
'in_progress:support-feedback',
|
|
237
|
+
'the person sees the status and feature staff set'
|
|
238
|
+
);
|
|
239
|
+
select is(pg_temp.affected($q$ update public.support_reports set message = 'changed' $q$), 0, 'a report staff picked up is no longer editable');
|
|
240
|
+
|
|
241
|
+
select throws_ok(
|
|
242
|
+
$$ select public.support_set_report_status(pg_temp.id('r1'), 'addressed') $$,
|
|
243
|
+
'42501', null, 'the person cannot set the status either'
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
-- Replies and read receipts -------------------------------------------------------------
|
|
247
|
+
insert into public.support_replies (report_id, body) values (pg_temp.id('r1'), 'It happens every time.');
|
|
248
|
+
reset role;
|
|
249
|
+
select is(
|
|
250
|
+
(select r.from_staff::text || ':' || (author_id = pg_temp.person('a1'))::text from public.support_replies r),
|
|
251
|
+
'false:true',
|
|
252
|
+
'the person''s reply is theirs and not staff''s'
|
|
253
|
+
);
|
|
254
|
+
set local role authenticated;
|
|
255
|
+
select ok(
|
|
256
|
+
(select user_read_at is not null and last_user_reply_at is not null from public.support_reports where id = pg_temp.id('r1')),
|
|
257
|
+
'replying marks the thread read for the person'
|
|
258
|
+
);
|
|
259
|
+
reset role;
|
|
260
|
+
|
|
261
|
+
select pg_temp.sign_in('b2');
|
|
262
|
+
set local role authenticated;
|
|
263
|
+
select throws_ok(
|
|
264
|
+
$$ insert into public.support_replies (report_id, body) values (pg_temp.id('r1'), 'hi') $$,
|
|
265
|
+
'42501', null, 'someone else cannot reply on the report'
|
|
266
|
+
);
|
|
267
|
+
select is((select count(*)::int from public.support_replies), 0, '... nor read its replies');
|
|
268
|
+
select throws_ok(
|
|
269
|
+
$$ select public.support_mark_read(pg_temp.id('r1')) $$,
|
|
270
|
+
'42501', null, '... nor mark it read'
|
|
271
|
+
);
|
|
272
|
+
reset role;
|
|
273
|
+
|
|
274
|
+
select pg_temp.sign_in('c3');
|
|
275
|
+
set local role authenticated;
|
|
276
|
+
insert into public.support_replies (report_id, body) values (pg_temp.id('r1'), 'Thanks, we are on it.');
|
|
277
|
+
select is((select count(*)::int from public.support_replies where from_staff), 1, 'a staff reply is marked as staff''s');
|
|
278
|
+
select ok(
|
|
279
|
+
(select last_staff_reply_at is not null and staff_read_at is not null from public.support_reports where id = pg_temp.id('r1')),
|
|
280
|
+
'a staff reply moves the thread on for the person'
|
|
281
|
+
);
|
|
282
|
+
reset role;
|
|
283
|
+
|
|
284
|
+
select pg_temp.sign_in('a1');
|
|
285
|
+
set local role authenticated;
|
|
286
|
+
select ok(
|
|
287
|
+
(select last_staff_reply_at > user_read_at from public.support_reports where id = pg_temp.id('r1')),
|
|
288
|
+
'the person has an unread staff reply'
|
|
289
|
+
);
|
|
290
|
+
select lives_ok($$ select public.support_mark_read(pg_temp.id('r1')) $$, 'the person marks it read');
|
|
291
|
+
select ok(
|
|
292
|
+
(select user_read_at >= last_staff_reply_at from public.support_reports where id = pg_temp.id('r1')),
|
|
293
|
+
'... and it is read'
|
|
294
|
+
);
|
|
295
|
+
select is((select count(*)::int from public.support_replies), 2, 'the person reads both sides of the thread');
|
|
296
|
+
reset role;
|
|
297
|
+
select pg_temp.remember('r3', $$ insert into public.support_reports (user_id, message) values ('00000000-0000-4000-8000-0000000000a1', 'Answered') returning id $$);
|
|
298
|
+
select pg_temp.sign_in('c3');
|
|
299
|
+
set local role authenticated;
|
|
300
|
+
insert into public.support_replies (report_id, body) values (pg_temp.id('r3'), 'Here is how');
|
|
301
|
+
reset role;
|
|
302
|
+
select pg_temp.sign_in('a1');
|
|
303
|
+
set local role authenticated;
|
|
304
|
+
select is(pg_temp.affected($q$ update public.support_reports set message = 'Rewritten' where id = pg_temp.id('r3') $q$), 0, 'once staff answer, the report is no longer editable');
|
|
305
|
+
|
|
306
|
+
-- Screenshots ---------------------------------------------------------------------------
|
|
307
|
+
select pg_temp.remember('r2', $$ insert into public.support_reports (message) values ('Layout is off') returning id $$);
|
|
308
|
+
select lives_ok(
|
|
309
|
+
format($$ insert into storage.objects (bucket_id, name, owner_id) values ('genesis-support', '%s/%s/shot.png', '%s') $$,
|
|
310
|
+
pg_temp.person('a1'), pg_temp.id('r2'), pg_temp.person('a1')),
|
|
311
|
+
'a person uploads into their open report''s folder'
|
|
312
|
+
);
|
|
313
|
+
select throws_ok(
|
|
314
|
+
format($$ insert into storage.objects (bucket_id, name) values ('genesis-support', '%s/%s/x.png') $$, pg_temp.person('b2'), pg_temp.id('r2')),
|
|
315
|
+
'42501', null, 'not into someone else''s folder'
|
|
316
|
+
);
|
|
317
|
+
select throws_ok(
|
|
318
|
+
format($$ insert into storage.objects (bucket_id, name) values ('genesis-support', '%s/%s/x.png') $$, pg_temp.person('a1'), pg_temp.id('r1')),
|
|
319
|
+
'42501', null, 'not into a report staff already picked up'
|
|
320
|
+
);
|
|
321
|
+
select throws_ok(
|
|
322
|
+
format($$ insert into storage.objects (bucket_id, name) values ('genesis-support', '%s/%s/x.png') $$, pg_temp.person('a1'), gen_random_uuid()),
|
|
323
|
+
'42501', null, 'not into a report that isn''t there'
|
|
324
|
+
);
|
|
325
|
+
select is(
|
|
326
|
+
pg_temp.affected($q$ update storage.objects set name = name || '.png' where bucket_id = 'genesis-support' $q$),
|
|
327
|
+
0,
|
|
328
|
+
'not over an existing file'
|
|
329
|
+
);
|
|
330
|
+
select lives_ok(
|
|
331
|
+
format($$ insert into public.support_attachments (report_id, path, content_type, byte_size, width, height)
|
|
332
|
+
values ('%s', '%s/%s/shot.png', 'image/png', 1024, 390, 844) $$,
|
|
333
|
+
pg_temp.id('r2'), pg_temp.person('a1'), pg_temp.id('r2')),
|
|
334
|
+
'the person records the screenshot they uploaded'
|
|
335
|
+
);
|
|
336
|
+
select throws_ok(
|
|
337
|
+
format($$ insert into public.support_attachments (report_id, path, content_type) values ('%s', '%s/%s/missing.png', 'image/png') $$,
|
|
338
|
+
pg_temp.id('r2'), pg_temp.person('a1'), pg_temp.id('r2')),
|
|
339
|
+
'23503', null, 'an attachment must name an uploaded file'
|
|
340
|
+
);
|
|
341
|
+
select throws_ok(
|
|
342
|
+
format($$ insert into public.support_attachments (report_id, path, content_type) values ('%s', '%s/%s/shot.png', 'image/png') $$,
|
|
343
|
+
pg_temp.id('r1'), pg_temp.person('a1'), pg_temp.id('r2')),
|
|
344
|
+
'42501', null, 'an attachment must live under its own report'
|
|
345
|
+
);
|
|
346
|
+
select throws_ok(
|
|
347
|
+
format($$ insert into public.support_attachments (report_id, path, content_type) values ('%s', '%s/%s/shot.png', 'image/gif') $$,
|
|
348
|
+
pg_temp.id('r2'), pg_temp.person('a1'), pg_temp.id('r2')),
|
|
349
|
+
'23514', null, 'only png, jpeg and webp'
|
|
350
|
+
);
|
|
351
|
+
select is((select count(*)::int from storage.objects where bucket_id = 'genesis-support'), 1, 'the person sees their screenshot');
|
|
352
|
+
select throws_ok(
|
|
353
|
+
format($$ insert into storage.objects (bucket_id, name) values ('genesis-support', '%s/%s/../../x.png') $$, pg_temp.person('a1'), pg_temp.id('r2')),
|
|
354
|
+
'42501', null, 'not under a name that isn''t a plain image name'
|
|
355
|
+
);
|
|
356
|
+
select throws_ok(
|
|
357
|
+
format($$ insert into storage.objects (bucket_id, name) values ('genesis-support', '%s/%s/run.exe') $$, pg_temp.person('a1'), pg_temp.id('r2')),
|
|
358
|
+
'42501', null, 'not a file that isn''t an image'
|
|
359
|
+
);
|
|
360
|
+
select lives_ok(
|
|
361
|
+
format($$ insert into storage.objects (bucket_id, name) select 'genesis-support', '%s/%s/more-' || g || '.png' from generate_series(1, 4) g $$, pg_temp.person('a1'), pg_temp.id('r2')),
|
|
362
|
+
'up to five objects in a report''s folder'
|
|
363
|
+
);
|
|
364
|
+
select throws_ok(
|
|
365
|
+
format($$ insert into storage.objects (bucket_id, name) values ('genesis-support', '%s/%s/sixth.png') $$, pg_temp.person('a1'), pg_temp.id('r2')),
|
|
366
|
+
'42501', null, '... and no sixth'
|
|
367
|
+
);
|
|
368
|
+
reset role;
|
|
369
|
+
-- An app that already lets everyone read, and owners update, their storage objects.
|
|
370
|
+
create policy genesis_test_read_everything on storage.objects for select to authenticated using (true);
|
|
371
|
+
create policy genesis_test_owners_update on storage.objects for update to authenticated using (true) with check (true);
|
|
372
|
+
select pg_temp.sign_in('b2');
|
|
373
|
+
set local role authenticated;
|
|
374
|
+
select is((select count(*)::int from storage.objects where bucket_id = 'genesis-support'), 0, 'a broad app read policy still shows nobody else''s screenshot');
|
|
375
|
+
reset role;
|
|
376
|
+
select pg_temp.sign_in('a1');
|
|
377
|
+
set local role authenticated;
|
|
378
|
+
select is(
|
|
379
|
+
pg_temp.affected($q$ update storage.objects set name = name || '-moved.png' where bucket_id = 'genesis-support' $q$),
|
|
380
|
+
0,
|
|
381
|
+
'a broad app update policy still moves no screenshot'
|
|
382
|
+
);
|
|
383
|
+
reset role;
|
|
384
|
+
drop policy genesis_test_read_everything on storage.objects;
|
|
385
|
+
drop policy genesis_test_owners_update on storage.objects;
|
|
386
|
+
select pg_temp.sign_in('a1');
|
|
387
|
+
set local role authenticated;
|
|
388
|
+
reset role;
|
|
389
|
+
|
|
390
|
+
select pg_temp.sign_in('b2');
|
|
391
|
+
set local role authenticated;
|
|
392
|
+
select is((select count(*)::int from storage.objects where bucket_id = 'genesis-support'), 0, 'someone else sees no screenshot');
|
|
393
|
+
select is((select count(*)::int from public.support_attachments), 0, '... and no attachment');
|
|
394
|
+
select throws_ok($$ delete from public.support_attachments $$, '42501', null, '... and deletes none');
|
|
395
|
+
reset role;
|
|
396
|
+
|
|
397
|
+
set local request.jwt.claims = '{"role": "anon"}';
|
|
398
|
+
set local role anon;
|
|
399
|
+
select is((select count(*)::int from storage.objects where bucket_id = 'genesis-support'), 0, 'anon sees no screenshot');
|
|
400
|
+
reset role;
|
|
401
|
+
|
|
402
|
+
select pg_temp.sign_in('c3');
|
|
403
|
+
set local role authenticated;
|
|
404
|
+
select is((select count(*)::int from storage.objects where bucket_id = 'genesis-support'), 1, 'staff see the screenshot');
|
|
405
|
+
select is((select count(*)::int from public.support_attachments), 1, '... and its attachment');
|
|
406
|
+
reset role;
|
|
407
|
+
|
|
408
|
+
-- Rate limits -------------------------------------------------------------------------
|
|
409
|
+
select pg_temp.sign_in('e5');
|
|
410
|
+
set local role authenticated;
|
|
411
|
+
insert into public.support_reports (message) select 'report ' || g from generate_series(1, 20) g;
|
|
412
|
+
select throws_ok($$ insert into public.support_reports (message) values ('one more') $$, 'GS429', null, 'the 21st report in an hour is refused');
|
|
413
|
+
select is(pg_temp.affected($q$ delete from public.support_reports $q$), 20, 'deleting them all');
|
|
414
|
+
select throws_ok($$ insert into public.support_reports (message) values ('and again') $$, 'GS429', null, '... doesn''t reset the limit');
|
|
415
|
+
reset role;
|
|
416
|
+
|
|
417
|
+
-- Ideas and votes ---------------------------------------------------------------------
|
|
418
|
+
select pg_temp.sign_in('a1');
|
|
419
|
+
set local role authenticated;
|
|
420
|
+
select pg_temp.remember('i1', $$ insert into public.support_ideas (title, body) values ('Dark mode for charts', 'Please') returning id $$);
|
|
421
|
+
select is(pg_temp.affected($q$ update public.support_ideas set title = 'Dark mode for every chart' $q$), 1, 'the author edits their open idea before anyone votes');
|
|
422
|
+
select is((select count(*)::int from public.support_my_idea_ids()), 1, 'support_my_idea_ids answers which ideas are mine');
|
|
423
|
+
select is((select status || ':' || vote_count from public.support_ideas), 'open:0', 'a new idea is open with no votes');
|
|
424
|
+
select throws_ok($$ insert into public.support_ideas (title, status) values ('Ship it now', 'shipped') $$, '42501', null, 'a person cannot set an idea''s status');
|
|
425
|
+
select throws_ok($$ insert into public.support_ideas (title, vote_count) values ('Lots of votes', 999) $$, '42501', null, '... or its votes');
|
|
426
|
+
select throws_ok($$ update public.support_ideas set vote_count = 999 $$, '42501', null, '... or change its count');
|
|
427
|
+
reset role;
|
|
428
|
+
|
|
429
|
+
select pg_temp.sign_in('d4');
|
|
430
|
+
set local role authenticated;
|
|
431
|
+
select throws_ok($$ insert into public.support_ideas (title) values ('From a throwaway login') $$, '42501', null, 'an anonymous sign-in cannot suggest');
|
|
432
|
+
select throws_ok(
|
|
433
|
+
$$ insert into public.support_idea_votes (idea_id) values (pg_temp.id('i1')) $$,
|
|
434
|
+
'42501', null, '... or vote'
|
|
435
|
+
);
|
|
436
|
+
select is((select count(*)::int from public.support_ideas), 1, '... but reads the board');
|
|
437
|
+
reset role;
|
|
438
|
+
select set_config('request.jwt.claims', json_build_object('sub', pg_temp.person('d4'), 'role', 'authenticated')::text, true);
|
|
439
|
+
set local role authenticated;
|
|
440
|
+
select throws_ok(
|
|
441
|
+
$$ insert into public.support_idea_votes (idea_id) values (pg_temp.id('i1')) $$,
|
|
442
|
+
'42501', null, 'still refused when a token hook drops the is_anonymous claim (auth.users decides)'
|
|
443
|
+
);
|
|
444
|
+
reset role;
|
|
445
|
+
|
|
446
|
+
set local request.jwt.claims = '{"role": "anon"}';
|
|
447
|
+
set local role anon;
|
|
448
|
+
select throws_ok($$ select count(*) from public.support_ideas $$, '42501', null, 'anon cannot read the board');
|
|
449
|
+
reset role;
|
|
450
|
+
|
|
451
|
+
select pg_temp.sign_in('b2');
|
|
452
|
+
set local role authenticated;
|
|
453
|
+
select is((select count(*)::int from public.support_ideas), 1, 'every signed-in person reads the board');
|
|
454
|
+
select lives_ok($$ insert into public.support_idea_votes (idea_id) values (pg_temp.id('i1')) $$, 'a person votes');
|
|
455
|
+
select throws_ok(
|
|
456
|
+
$$ insert into public.support_idea_votes (idea_id) values (pg_temp.id('i1')) $$,
|
|
457
|
+
'23505', null, 'a second vote on the same idea is refused'
|
|
458
|
+
);
|
|
459
|
+
select throws_ok(
|
|
460
|
+
format($$ insert into public.support_idea_votes (idea_id, user_id) values ('%s', '%s') $$, pg_temp.id('i1'), pg_temp.person('e5')),
|
|
461
|
+
'42501', null, 'nobody votes as someone else'
|
|
462
|
+
);
|
|
463
|
+
select is((select vote_count from public.support_ideas where id = pg_temp.id('i1')), 1, 'the idea counts one vote');
|
|
464
|
+
select is(pg_temp.affected($q$ update public.support_ideas set title = 'Taken over' $q$), 0, 'someone else cannot edit the idea');
|
|
465
|
+
reset role;
|
|
466
|
+
|
|
467
|
+
select pg_temp.sign_in('a1');
|
|
468
|
+
set local role authenticated;
|
|
469
|
+
select is((select count(*)::int from public.support_idea_votes), 0, 'votes are private: the author doesn''t see who voted');
|
|
470
|
+
select is(pg_temp.affected($q$ update public.support_ideas set title = 'Something else' $q$), 0, 'once someone has voted, the idea can''t be retitled');
|
|
471
|
+
reset role;
|
|
472
|
+
|
|
473
|
+
select pg_temp.sign_in('c3');
|
|
474
|
+
set local role authenticated;
|
|
475
|
+
select lives_ok(
|
|
476
|
+
$$ select public.support_set_idea_status(pg_temp.id('i1'), 'shipped', 'charts-dark-mode') $$,
|
|
477
|
+
'staff mark the idea shipped, with the FeatureId that delivered it'
|
|
478
|
+
);
|
|
479
|
+
select throws_ok(
|
|
480
|
+
$$ select public.support_set_idea_status(pg_temp.id('i1'), 'shipped', 'Not A Feature Id') $$,
|
|
481
|
+
'23514', null, 'a feature id must be a FeatureId'
|
|
482
|
+
);
|
|
483
|
+
reset role;
|
|
484
|
+
|
|
485
|
+
select pg_temp.sign_in('b2');
|
|
486
|
+
set local role authenticated;
|
|
487
|
+
select is(
|
|
488
|
+
(select feature_id from public.support_shipped_for_me()),
|
|
489
|
+
'charts-dark-mode',
|
|
490
|
+
'you asked, we shipped: the voter sees it with its feature'
|
|
491
|
+
);
|
|
492
|
+
select lives_ok($$ delete from public.support_idea_votes where idea_id = pg_temp.id('i1') $$, 'a person withdraws their vote');
|
|
493
|
+
select is((select vote_count from public.support_ideas where id = pg_temp.id('i1')), 0, 'the count goes down');
|
|
494
|
+
select throws_ok(
|
|
495
|
+
$$ insert into public.support_idea_votes (idea_id) values (pg_temp.id('i1')) $$,
|
|
496
|
+
'42501', null, 'a shipped idea takes no new votes'
|
|
497
|
+
);
|
|
498
|
+
reset role;
|
|
499
|
+
|
|
500
|
+
select pg_temp.sign_in('a1');
|
|
501
|
+
set local role authenticated;
|
|
502
|
+
select is((select count(*)::int from public.support_shipped_for_me()), 1, 'the author sees it shipped too');
|
|
503
|
+
select is(pg_temp.affected($q$ update public.support_ideas set title = 'Changed after shipping' $q$), 0, 'a shipped idea is no longer editable');
|
|
504
|
+
reset role;
|
|
505
|
+
|
|
506
|
+
select pg_temp.sign_in('e5');
|
|
507
|
+
set local role authenticated;
|
|
508
|
+
select is((select count(*)::int from public.support_shipped_for_me()), 0, 'someone who neither asked nor voted doesn''t');
|
|
509
|
+
reset role;
|
|
510
|
+
|
|
511
|
+
select pg_temp.sign_in('c3');
|
|
512
|
+
set local role authenticated;
|
|
513
|
+
select lives_ok($$ select public.support_set_idea_status(pg_temp.id('i1'), 'hidden') $$, 'staff hide an idea');
|
|
514
|
+
select is((select shipped_at from public.support_ideas where id = pg_temp.id('i1')), null, 'leaving shipped clears shipped_at');
|
|
515
|
+
reset role;
|
|
516
|
+
|
|
517
|
+
select pg_temp.sign_in('b2');
|
|
518
|
+
set local role authenticated;
|
|
519
|
+
select is((select count(*)::int from public.support_ideas), 0, 'a hidden idea is off the board');
|
|
520
|
+
reset role;
|
|
521
|
+
|
|
522
|
+
-- Deleting ----------------------------------------------------------------------------
|
|
523
|
+
select pg_temp.sign_in('a1');
|
|
524
|
+
set local role authenticated;
|
|
525
|
+
select is(pg_temp.affected($q$ delete from public.support_reports where id = pg_temp.id('r2') $q$), 1, 'a person deletes their own report');
|
|
526
|
+
reset role;
|
|
527
|
+
select is(
|
|
528
|
+
(select count(*)::int from public.support_attachment_purges where path like pg_temp.person('a1')::text || '/%'),
|
|
529
|
+
1,
|
|
530
|
+
'its screenshot is queued for removal from storage'
|
|
531
|
+
);
|
|
532
|
+
select pg_temp.sign_in('c3');
|
|
533
|
+
set local role authenticated;
|
|
534
|
+
select is((select count(*)::int from storage.objects where bucket_id = 'genesis-support'), 0, 'staff can no longer open a deleted report''s screenshots');
|
|
535
|
+
reset role;
|
|
536
|
+
select is(
|
|
537
|
+
(select count(*)::int from public.support_orphan_objects(interval '0 seconds') where path like pg_temp.person('a1')::text || '/%'),
|
|
538
|
+
5,
|
|
539
|
+
'and an object no attachment names is listed as orphaned'
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
-- Deleting the login takes every report, reply and vote with it, and keeps ideas without the person.
|
|
543
|
+
delete from auth.users where id = pg_temp.person('a1');
|
|
544
|
+
select is((select count(*)::int from public.support_reports where user_id = pg_temp.person('a1')), 0, 'a deleted login''s reports go');
|
|
545
|
+
select is(
|
|
546
|
+
(select count(*)::int from public.support_ideas where id = pg_temp.id('i1') and author_id is null),
|
|
547
|
+
1,
|
|
548
|
+
'its ideas stay, without the person'
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
select * from finish();
|
|
552
|
+
rollback;
|