@marktiderman/genesis-supabase 0.1.1 → 0.3.0
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 +523 -1
- 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 -78
- package/dist/index.js +63 -2
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +233 -5
- package/dist/native.js +196 -1
- 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/0001_genesis_account_deletion.sql +133 -0
- package/migrations/0002_genesis_app_release_policy.sql +82 -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/0001_genesis_account_deletion.test.sql +138 -0
- package/migrations/tests/0002_genesis_app_release_policy.test.sql +148 -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 +92 -4
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
-- @feat:tenant-features
|
|
2
|
+
-- pgTAP test for 0004_genesis_tenant_features.sql.
|
|
3
|
+
-- Copy into your app's `supabase/tests/database/` and run `supabase test db`.
|
|
4
|
+
-- It writes only its own users, tenants and hooks and rolls everything back.
|
|
5
|
+
-- If your app already has the hooks (`private.genesis_tenant_admin`,
|
|
6
|
+
-- `private.genesis_tenant_member`), they are dropped and replaced inside this
|
|
7
|
+
-- transaction only.
|
|
8
|
+
--
|
|
9
|
+
-- Signing in: auth.uid() reads request.jwt.claims (and the legacy
|
|
10
|
+
-- request.jwt.claim.sub on older images), so pg_temp.sign_in sets both.
|
|
11
|
+
begin;
|
|
12
|
+
create extension if not exists pgtap with schema extensions;
|
|
13
|
+
select plan(65);
|
|
14
|
+
|
|
15
|
+
-- People: a1 and e5 admins of tenant 1, b2 member of tenant 1, c3 admin of tenant 2,
|
|
16
|
+
-- d4 nobody's admin but the owner of tenant 3.
|
|
17
|
+
insert into auth.users (id, aud, role, email, instance_id)
|
|
18
|
+
select ('00000000-0000-4000-8000-0000000000' || n)::uuid, 'authenticated', 'authenticated', 'genesis-tenant-' || n || '@example.test', '00000000-0000-0000-0000-000000000000'
|
|
19
|
+
from unnest(array['a1', 'b2', 'c3', 'd4', 'e5']) n;
|
|
20
|
+
|
|
21
|
+
create function pg_temp.person(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-0000000000' || n)::uuid $$;
|
|
22
|
+
create function pg_temp.tenant(n int) returns uuid language sql immutable as $$ select ('00000000-0000-4000-9000-00000000000' || 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', json_build_object('sub', pg_temp.person(n), 'role', 'authenticated')::text, true);
|
|
26
|
+
perform set_config('request.jwt.claim.sub', pg_temp.person(n)::text, true);
|
|
27
|
+
end $$;
|
|
28
|
+
grant execute on function pg_temp.person(text), pg_temp.tenant(int), pg_temp.sign_in(text) to anon, authenticated, service_role;
|
|
29
|
+
|
|
30
|
+
-- An app's tenant table, with the foreign keys the migration's header recommends.
|
|
31
|
+
-- A tenant owned by a person goes when they do (a personal workspace).
|
|
32
|
+
create table public.genesis_test_teams (id uuid primary key, owner uuid references auth.users (id) on delete cascade);
|
|
33
|
+
insert into public.genesis_test_teams values (pg_temp.tenant(1), null), (pg_temp.tenant(2), null), (pg_temp.tenant(3), pg_temp.person('d4'));
|
|
34
|
+
alter table public.tenant_features add foreign key (tenant_id) references public.genesis_test_teams (id) on delete cascade;
|
|
35
|
+
alter table public.tenant_feature_changes add foreign key (tenant_id) references public.genesis_test_teams (id) on delete cascade;
|
|
36
|
+
|
|
37
|
+
drop function if exists private.genesis_tenant_admin(uuid, uuid);
|
|
38
|
+
drop function if exists private.genesis_tenant_member(uuid, uuid);
|
|
39
|
+
|
|
40
|
+
-- Shape -------------------------------------------------------------------------
|
|
41
|
+
select has_table('public', 'tenant_features', 'tenant_features exists');
|
|
42
|
+
select col_is_pk('public', 'tenant_features', array['tenant_id', 'feature_id'], 'one switch per tenant and feature');
|
|
43
|
+
select has_table('public', 'tenant_feature_changes', 'tenant_feature_changes exists');
|
|
44
|
+
select ok((select relrowsecurity from pg_class where oid = 'public.tenant_features'::regclass), 'RLS is on for the switches');
|
|
45
|
+
select ok((select relrowsecurity from pg_class where oid = 'public.tenant_feature_changes'::regclass), 'RLS is on for the history');
|
|
46
|
+
select is(
|
|
47
|
+
(select count(*)::int from pg_policy where polrelid in ('public.tenant_features'::regclass, 'public.tenant_feature_changes'::regclass)),
|
|
48
|
+
0,
|
|
49
|
+
'no policies: the tables are reached only through the functions'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Privileges ------------------------------------------------------------------------
|
|
53
|
+
select ok(
|
|
54
|
+
not has_table_privilege('anon', 'public.tenant_features', 'select,insert,update,delete,truncate')
|
|
55
|
+
and not has_table_privilege('authenticated', 'public.tenant_features', 'select,insert,update,delete,truncate'),
|
|
56
|
+
'anon and authenticated have no privilege on the switches'
|
|
57
|
+
);
|
|
58
|
+
select ok(
|
|
59
|
+
not has_table_privilege('anon', 'public.tenant_feature_changes', 'select,insert,update,delete,truncate')
|
|
60
|
+
and not has_table_privilege('authenticated', 'public.tenant_feature_changes', 'select,insert,update,delete,truncate'),
|
|
61
|
+
'... nor on the history'
|
|
62
|
+
);
|
|
63
|
+
select ok(
|
|
64
|
+
not exists (
|
|
65
|
+
select 1 from pg_class c, aclexplode(coalesce(c.relacl, acldefault('r', c.relowner))) a
|
|
66
|
+
where c.oid in ('public.tenant_features'::regclass, 'public.tenant_feature_changes'::regclass) and a.grantee = 0
|
|
67
|
+
),
|
|
68
|
+
'public has no grant on either table'
|
|
69
|
+
);
|
|
70
|
+
select ok(
|
|
71
|
+
has_table_privilege('service_role', 'public.tenant_features', 'insert')
|
|
72
|
+
and has_table_privilege('service_role', 'public.tenant_features', 'update')
|
|
73
|
+
and has_table_privilege('service_role', 'public.tenant_features', 'delete'),
|
|
74
|
+
'the service role writes the switches'
|
|
75
|
+
);
|
|
76
|
+
select ok(
|
|
77
|
+
not has_table_privilege('service_role', 'public.tenant_feature_changes', 'insert')
|
|
78
|
+
and not has_table_privilege('service_role', 'public.tenant_feature_changes', 'update')
|
|
79
|
+
and has_table_privilege('service_role', 'public.tenant_feature_changes', 'delete'),
|
|
80
|
+
'the service role can purge the history but never write it'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
select ok(
|
|
84
|
+
(select bool_and(prosecdef and proconfig @> array['search_path=""'] and proowner::regrole::text = 'postgres') from pg_proc
|
|
85
|
+
where oid in (
|
|
86
|
+
'public.set_tenant_feature(uuid, text, boolean, text)'::regprocedure,
|
|
87
|
+
'public.tenant_features_off(uuid)'::regprocedure,
|
|
88
|
+
'public.tenant_feature_settings(uuid)'::regprocedure,
|
|
89
|
+
'public.tenant_feature_history(uuid, integer)'::regprocedure,
|
|
90
|
+
'private.tenant_feature_on(uuid, text)'::regprocedure,
|
|
91
|
+
'private.genesis_is_tenant_admin(uuid)'::regprocedure,
|
|
92
|
+
'private.genesis_is_tenant_member(uuid)'::regprocedure,
|
|
93
|
+
'private.genesis_record_tenant_feature_change()'::regprocedure
|
|
94
|
+
)),
|
|
95
|
+
'every function is security definer, owned by postgres, with an empty search_path'
|
|
96
|
+
);
|
|
97
|
+
select ok(
|
|
98
|
+
not has_function_privilege('anon', 'public.set_tenant_feature(uuid, text, boolean, text)', 'execute')
|
|
99
|
+
and not has_function_privilege('anon', 'public.tenant_features_off(uuid)', 'execute')
|
|
100
|
+
and not has_function_privilege('anon', 'public.tenant_feature_settings(uuid)', 'execute')
|
|
101
|
+
and not has_function_privilege('anon', 'public.tenant_feature_history(uuid, integer)', 'execute'),
|
|
102
|
+
'anon executes none of the public functions'
|
|
103
|
+
);
|
|
104
|
+
select ok(
|
|
105
|
+
has_function_privilege('authenticated', 'public.set_tenant_feature(uuid, text, boolean, text)', 'execute')
|
|
106
|
+
and has_function_privilege('authenticated', 'public.tenant_features_off(uuid)', 'execute')
|
|
107
|
+
and has_function_privilege('authenticated', 'public.tenant_feature_settings(uuid)', 'execute')
|
|
108
|
+
and has_function_privilege('authenticated', 'public.tenant_feature_history(uuid, integer)', 'execute'),
|
|
109
|
+
'authenticated executes them (each asks the hook)'
|
|
110
|
+
);
|
|
111
|
+
select ok(
|
|
112
|
+
not exists (
|
|
113
|
+
select 1 from pg_proc p, aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) a
|
|
114
|
+
where p.oid in (
|
|
115
|
+
'public.set_tenant_feature(uuid, text, boolean, text)'::regprocedure,
|
|
116
|
+
'public.tenant_features_off(uuid)'::regprocedure,
|
|
117
|
+
'public.tenant_feature_settings(uuid)'::regprocedure,
|
|
118
|
+
'public.tenant_feature_history(uuid, integer)'::regprocedure,
|
|
119
|
+
'private.tenant_feature_on(uuid, text)'::regprocedure,
|
|
120
|
+
'private.genesis_is_tenant_admin(uuid)'::regprocedure,
|
|
121
|
+
'private.genesis_is_tenant_member(uuid)'::regprocedure,
|
|
122
|
+
'private.genesis_record_tenant_feature_change()'::regprocedure
|
|
123
|
+
) and a.grantee = 0 and a.privilege_type = 'EXECUTE'
|
|
124
|
+
),
|
|
125
|
+
'public executes none of them'
|
|
126
|
+
);
|
|
127
|
+
select ok(
|
|
128
|
+
not has_function_privilege('anon', 'private.genesis_is_tenant_admin(uuid)', 'execute')
|
|
129
|
+
and not has_function_privilege('authenticated', 'private.genesis_is_tenant_admin(uuid)', 'execute')
|
|
130
|
+
and not has_function_privilege('authenticated', 'private.genesis_is_tenant_member(uuid)', 'execute')
|
|
131
|
+
and not has_function_privilege('authenticated', 'private.genesis_record_tenant_feature_change()', 'execute'),
|
|
132
|
+
'the hook checks and the trigger are not executable by anon or authenticated'
|
|
133
|
+
);
|
|
134
|
+
select ok(
|
|
135
|
+
not has_function_privilege('anon', 'private.tenant_feature_on(uuid, text)', 'execute')
|
|
136
|
+
and not has_function_privilege('authenticated', 'private.tenant_feature_on(uuid, text)', 'execute'),
|
|
137
|
+
'the SQL gate (no member check) is for definer functions, not anon or authenticated'
|
|
138
|
+
);
|
|
139
|
+
select ok(
|
|
140
|
+
not has_sequence_privilege('anon', 'public.tenant_feature_changes_id_seq', 'usage,select,update')
|
|
141
|
+
and not has_sequence_privilege('authenticated', 'public.tenant_feature_changes_id_seq', 'usage,select,update'),
|
|
142
|
+
'the history''s id sequence is not anon''s or authenticated''s'
|
|
143
|
+
);
|
|
144
|
+
select ok(
|
|
145
|
+
not has_table_privilege('service_role', 'public.tenant_features', 'truncate,references,trigger'),
|
|
146
|
+
'the service role cannot truncate the switches (which would skip the history)'
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
-- Without the hook, nobody may change anything --------------------------------------
|
|
150
|
+
select pg_temp.sign_in('a1');
|
|
151
|
+
set local role authenticated;
|
|
152
|
+
select throws_ok(
|
|
153
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'Not yet') $$,
|
|
154
|
+
'42501', 'set_tenant_feature: nobody may change tenant features yet',
|
|
155
|
+
'without private.genesis_tenant_admin, set_tenant_feature refuses (fails closed)'
|
|
156
|
+
);
|
|
157
|
+
select throws_ok(
|
|
158
|
+
$$ select * from public.tenant_features_off(pg_temp.tenant(1)) $$,
|
|
159
|
+
'42501', null,
|
|
160
|
+
'... and nobody may read a tenant''s switches'
|
|
161
|
+
);
|
|
162
|
+
reset role;
|
|
163
|
+
|
|
164
|
+
-- The app's hooks, reading a membership table of the test's own --------------------
|
|
165
|
+
create table private.genesis_test_members (tenant uuid, person uuid, admin boolean);
|
|
166
|
+
insert into private.genesis_test_members values
|
|
167
|
+
(pg_temp.tenant(1), pg_temp.person('a1'), true),
|
|
168
|
+
(pg_temp.tenant(1), pg_temp.person('e5'), true),
|
|
169
|
+
(pg_temp.tenant(1), pg_temp.person('b2'), false),
|
|
170
|
+
(pg_temp.tenant(2), pg_temp.person('c3'), true);
|
|
171
|
+
create function private.genesis_tenant_admin(tenant uuid, person uuid) returns boolean
|
|
172
|
+
language sql stable set search_path = '' as $$
|
|
173
|
+
select exists (select 1 from private.genesis_test_members m where m.tenant = $1 and m.person = $2 and m.admin)
|
|
174
|
+
$$;
|
|
175
|
+
create function private.genesis_tenant_member(tenant uuid, person uuid) returns boolean
|
|
176
|
+
language sql stable set search_path = '' as $$
|
|
177
|
+
select exists (select 1 from private.genesis_test_members m where m.tenant = $1 and m.person = $2)
|
|
178
|
+
$$;
|
|
179
|
+
revoke execute on function private.genesis_tenant_admin(uuid, uuid) from public, anon, authenticated;
|
|
180
|
+
revoke execute on function private.genesis_tenant_member(uuid, uuid) from public, anon, authenticated;
|
|
181
|
+
|
|
182
|
+
-- Who may change a switch ------------------------------------------------------------
|
|
183
|
+
set local role anon;
|
|
184
|
+
select throws_ok(
|
|
185
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'x') $$,
|
|
186
|
+
'42501', null, 'anon cannot call set_tenant_feature'
|
|
187
|
+
);
|
|
188
|
+
reset role;
|
|
189
|
+
|
|
190
|
+
select set_config('request.jwt.claims', '{"role":"authenticated"}', true);
|
|
191
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
192
|
+
set local role authenticated;
|
|
193
|
+
select throws_ok(
|
|
194
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'x') $$,
|
|
195
|
+
'42501', 'set_tenant_feature: not signed in', 'a call with nobody signed in is refused'
|
|
196
|
+
);
|
|
197
|
+
reset role;
|
|
198
|
+
|
|
199
|
+
select pg_temp.sign_in('b2');
|
|
200
|
+
set local role authenticated;
|
|
201
|
+
select throws_ok(
|
|
202
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'Members want it off') $$,
|
|
203
|
+
'42501', 'set_tenant_feature: only the tenant''s admins can change its features', 'a member who isn''t an admin is refused'
|
|
204
|
+
);
|
|
205
|
+
reset role;
|
|
206
|
+
|
|
207
|
+
select pg_temp.sign_in('c3');
|
|
208
|
+
set local role authenticated;
|
|
209
|
+
select throws_ok(
|
|
210
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'Not mine') $$,
|
|
211
|
+
'42501', null, 'another tenant''s admin is refused'
|
|
212
|
+
);
|
|
213
|
+
reset role;
|
|
214
|
+
|
|
215
|
+
select pg_temp.sign_in('a1');
|
|
216
|
+
set local role authenticated;
|
|
217
|
+
select throws_ok(
|
|
218
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, ' ') $$,
|
|
219
|
+
'22023', null, 'a blank reason is refused'
|
|
220
|
+
);
|
|
221
|
+
select throws_ok(
|
|
222
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, repeat('x', 501)) $$,
|
|
223
|
+
'22023', null, 'a reason over 500 characters is refused'
|
|
224
|
+
);
|
|
225
|
+
select throws_ok(
|
|
226
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', null, 'why') $$,
|
|
227
|
+
'22004', null, 'enabled must be given'
|
|
228
|
+
);
|
|
229
|
+
select throws_ok(
|
|
230
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'Not An Id', false, 'why') $$,
|
|
231
|
+
'23514', null, 'a feature id must have a FeatureId''s shape'
|
|
232
|
+
);
|
|
233
|
+
select lives_ok(
|
|
234
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, ' Launching next month ') $$,
|
|
235
|
+
'the tenant''s admin turns a feature off'
|
|
236
|
+
);
|
|
237
|
+
select lives_ok(
|
|
238
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'billing', false, 'Not offered here') $$,
|
|
239
|
+
'... and another'
|
|
240
|
+
);
|
|
241
|
+
select throws_ok(
|
|
242
|
+
$$ select * from public.tenant_features $$,
|
|
243
|
+
'42501', null, 'authenticated cannot read the table directly'
|
|
244
|
+
);
|
|
245
|
+
select throws_ok(
|
|
246
|
+
$$ insert into public.tenant_features (tenant_id, feature_id, enabled, reason) values (pg_temp.tenant(1), 'x', false, 'x') $$,
|
|
247
|
+
'42501', null, '... nor write it'
|
|
248
|
+
);
|
|
249
|
+
select throws_ok(
|
|
250
|
+
$$ select * from public.tenant_feature_changes $$,
|
|
251
|
+
'42501', null, '... nor read the history directly'
|
|
252
|
+
);
|
|
253
|
+
select throws_ok(
|
|
254
|
+
$$ select private.genesis_is_tenant_admin(pg_temp.tenant(1)) $$,
|
|
255
|
+
'42501', null, '... nor call the hook check itself'
|
|
256
|
+
);
|
|
257
|
+
reset role;
|
|
258
|
+
|
|
259
|
+
select is(
|
|
260
|
+
(select reason from public.tenant_features where tenant_id = pg_temp.tenant(1) and feature_id = 'chat'),
|
|
261
|
+
'Launching next month',
|
|
262
|
+
'the reason is kept, trimmed'
|
|
263
|
+
);
|
|
264
|
+
select is(
|
|
265
|
+
(select updated_by from public.tenant_features where tenant_id = pg_temp.tenant(1) and feature_id = 'chat'),
|
|
266
|
+
pg_temp.person('a1'),
|
|
267
|
+
'... with who set it'
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
select pg_temp.sign_in('e5');
|
|
271
|
+
set local role authenticated;
|
|
272
|
+
select lives_ok(
|
|
273
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'Launching next month') $$,
|
|
274
|
+
'another admin saves the same switch and reason'
|
|
275
|
+
);
|
|
276
|
+
reset role;
|
|
277
|
+
select is(
|
|
278
|
+
(select updated_by from public.tenant_features where tenant_id = pg_temp.tenant(1) and feature_id = 'chat'),
|
|
279
|
+
pg_temp.person('a1'),
|
|
280
|
+
'... which changes nothing, not even who set it'
|
|
281
|
+
);
|
|
282
|
+
select is(
|
|
283
|
+
(select count(*)::int from public.tenant_feature_changes where tenant_id = pg_temp.tenant(1) and feature_id = 'chat'),
|
|
284
|
+
1,
|
|
285
|
+
'... and records nothing'
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
-- Who may read ------------------------------------------------------------------------
|
|
289
|
+
select pg_temp.sign_in('b2');
|
|
290
|
+
set local role authenticated;
|
|
291
|
+
select is(
|
|
292
|
+
(select array_agg(f) from public.tenant_features_off(pg_temp.tenant(1)) f),
|
|
293
|
+
array['billing', 'chat'],
|
|
294
|
+
'a member reads the ids that are off, in order'
|
|
295
|
+
);
|
|
296
|
+
select throws_ok(
|
|
297
|
+
$$ select * from public.tenant_feature_settings(pg_temp.tenant(1)) $$,
|
|
298
|
+
'42501', null, '... but not the reasons (admins only)'
|
|
299
|
+
);
|
|
300
|
+
select throws_ok(
|
|
301
|
+
$$ select * from public.tenant_feature_history(pg_temp.tenant(1)) $$,
|
|
302
|
+
'42501', null, '... nor the history'
|
|
303
|
+
);
|
|
304
|
+
reset role;
|
|
305
|
+
|
|
306
|
+
select pg_temp.sign_in('c3');
|
|
307
|
+
set local role authenticated;
|
|
308
|
+
select throws_ok(
|
|
309
|
+
$$ select * from public.tenant_features_off(pg_temp.tenant(1)) $$,
|
|
310
|
+
'42501', null, 'someone outside the tenant cannot read its switches'
|
|
311
|
+
);
|
|
312
|
+
select is(
|
|
313
|
+
(select count(*)::int from public.tenant_features_off(pg_temp.tenant(2))),
|
|
314
|
+
0,
|
|
315
|
+
'a tenant with no switches has nothing off'
|
|
316
|
+
);
|
|
317
|
+
reset role;
|
|
318
|
+
|
|
319
|
+
select pg_temp.sign_in('a1');
|
|
320
|
+
set local role authenticated;
|
|
321
|
+
select is(
|
|
322
|
+
(select array_agg(feature_id || ':' || enabled || ':' || reason order by feature_id) from public.tenant_feature_settings(pg_temp.tenant(1))),
|
|
323
|
+
array['billing:false:Not offered here', 'chat:false:Launching next month'],
|
|
324
|
+
'the admin reads every switch with its reason'
|
|
325
|
+
);
|
|
326
|
+
select lives_ok(
|
|
327
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', true, 'Launched') $$,
|
|
328
|
+
'the admin turns it back on'
|
|
329
|
+
);
|
|
330
|
+
select is(
|
|
331
|
+
(select array_agg(f) from public.tenant_features_off(pg_temp.tenant(1)) f),
|
|
332
|
+
array['billing'],
|
|
333
|
+
'... and it is no longer off'
|
|
334
|
+
);
|
|
335
|
+
select is(
|
|
336
|
+
(select array_agg(enabled::text || ':' || reason || ':' || changed_role) from public.tenant_feature_history(pg_temp.tenant(1)) where feature_id = 'chat'),
|
|
337
|
+
array['true:Launched:authenticated', 'false:Launching next month:authenticated'],
|
|
338
|
+
'the history keeps both changes, newest first, with the role'
|
|
339
|
+
);
|
|
340
|
+
select is(
|
|
341
|
+
(select count(*)::int from public.tenant_feature_history(pg_temp.tenant(1), 1)),
|
|
342
|
+
1,
|
|
343
|
+
'max_rows limits the history'
|
|
344
|
+
);
|
|
345
|
+
select ok(
|
|
346
|
+
(select bool_and(changed_by = pg_temp.person('a1')) from public.tenant_feature_history(pg_temp.tenant(1))),
|
|
347
|
+
'every change records who made it'
|
|
348
|
+
);
|
|
349
|
+
reset role;
|
|
350
|
+
|
|
351
|
+
-- The SQL gate --------------------------------------------------------------------------
|
|
352
|
+
select ok(not private.tenant_feature_on(pg_temp.tenant(1), 'billing'), 'tenant_feature_on: off when turned off');
|
|
353
|
+
select ok(private.tenant_feature_on(pg_temp.tenant(1), 'chat'), '... on when turned back on');
|
|
354
|
+
select ok(private.tenant_feature_on(pg_temp.tenant(2), 'billing'), '... on with no row (another tenant''s switch doesn''t leak)');
|
|
355
|
+
|
|
356
|
+
-- The service role -------------------------------------------------------------------------
|
|
357
|
+
select set_config('request.jwt.claims', '{"role":"service_role"}', true);
|
|
358
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
359
|
+
set local role service_role;
|
|
360
|
+
select lives_ok(
|
|
361
|
+
$$ insert into public.tenant_features (tenant_id, feature_id, enabled, reason) values (pg_temp.tenant(2), 'chat', false, 'Platform: paused for review') $$,
|
|
362
|
+
'the service role writes a switch directly'
|
|
363
|
+
);
|
|
364
|
+
select throws_ok(
|
|
365
|
+
$$ insert into public.tenant_feature_changes (tenant_id, feature_id, enabled, reason, changed_role) values (pg_temp.tenant(2), 'chat', true, 'forged', 'authenticated') $$,
|
|
366
|
+
'42501', null, '... but cannot forge history'
|
|
367
|
+
);
|
|
368
|
+
reset role;
|
|
369
|
+
select is(
|
|
370
|
+
(select changed_role || ':' || coalesce(changed_by::text, 'nobody') from public.tenant_feature_changes where tenant_id = pg_temp.tenant(2)),
|
|
371
|
+
'service_role:nobody',
|
|
372
|
+
'its write is recorded, with no person'
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
-- A hook that answers null, or fails ----------------------------------------------------------
|
|
376
|
+
create or replace function private.genesis_tenant_admin(tenant uuid, person uuid) returns boolean
|
|
377
|
+
language sql stable set search_path = '' as $$ select null::boolean $$;
|
|
378
|
+
select pg_temp.sign_in('a1');
|
|
379
|
+
set local role authenticated;
|
|
380
|
+
select throws_ok(
|
|
381
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'x') $$,
|
|
382
|
+
'42501', null, 'a null answer from the hook is a no'
|
|
383
|
+
);
|
|
384
|
+
reset role;
|
|
385
|
+
create or replace function private.genesis_tenant_admin(tenant uuid, person uuid) returns boolean
|
|
386
|
+
language plpgsql stable set search_path = '' as $$ begin raise exception 'membership lookup failed'; end $$;
|
|
387
|
+
set local role authenticated;
|
|
388
|
+
select throws_ok(
|
|
389
|
+
$$ select public.set_tenant_feature(pg_temp.tenant(1), 'chat', false, 'x') $$,
|
|
390
|
+
'P0001', 'membership lookup failed', 'a hook that fails refuses the change'
|
|
391
|
+
);
|
|
392
|
+
reset role;
|
|
393
|
+
|
|
394
|
+
-- Deleting a tenant, or a person whose tenant goes with them, never fails on these tables ----------
|
|
395
|
+
select lives_ok($$ delete from public.genesis_test_teams where id = pg_temp.tenant(2) $$, 'deleting a tenant with switches and history succeeds');
|
|
396
|
+
select ok(
|
|
397
|
+
not exists (select 1 from public.tenant_features where tenant_id = pg_temp.tenant(2))
|
|
398
|
+
and not exists (select 1 from public.tenant_feature_changes where tenant_id = pg_temp.tenant(2)),
|
|
399
|
+
'... and takes them with it'
|
|
400
|
+
);
|
|
401
|
+
set local role service_role;
|
|
402
|
+
insert into public.tenant_features (tenant_id, feature_id, enabled, reason) values (pg_temp.tenant(3), 'chat', false, 'Platform: paused');
|
|
403
|
+
reset role;
|
|
404
|
+
-- As delete_my_account does it: the person's own JWT, then the delete.
|
|
405
|
+
select pg_temp.sign_in('d4');
|
|
406
|
+
select lives_ok($$ delete from auth.users where id = pg_temp.person('d4') $$, 'a person whose tenant (with a switch) goes with them can delete their account');
|
|
407
|
+
select ok(not exists (select 1 from public.tenant_features where tenant_id = pg_temp.tenant(3)), '... and the tenant''s switches go too');
|
|
408
|
+
|
|
409
|
+
-- Deleting a person never fails on these tables -------------------------------------------------
|
|
410
|
+
select lives_ok($$ delete from auth.users where id = pg_temp.person('a1') $$, 'deleting the admin who made changes succeeds');
|
|
411
|
+
select ok(
|
|
412
|
+
(select bool_and(changed_by is null) from public.tenant_feature_changes where tenant_id = pg_temp.tenant(1))
|
|
413
|
+
and (select bool_and(updated_by is null) from public.tenant_features where tenant_id = pg_temp.tenant(1)),
|
|
414
|
+
'... and their changes stay, without the person'
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
select * from finish();
|
|
418
|
+
rollback;
|