@marktiderman/genesis-notifications 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +251 -2
- package/dist/blocks.d.ts +10 -0
- package/dist/blocks.js +35 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chunk-2BG336II.js +124 -0
- package/dist/chunk-2BG336II.js.map +1 -0
- package/dist/chunk-7PHA3WVG.js +51 -0
- package/dist/chunk-7PHA3WVG.js.map +1 -0
- package/dist/chunk-EWZB35V6.js +113 -0
- package/dist/chunk-EWZB35V6.js.map +1 -0
- package/dist/chunk-UXNWI7OS.js +118 -0
- package/dist/chunk-UXNWI7OS.js.map +1 -0
- package/dist/email-render-CID_pXDy.d.ts +53 -0
- package/dist/email.d.ts +46 -0
- package/dist/email.js +64 -0
- package/dist/email.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +92 -0
- package/dist/native.js +170 -0
- package/dist/native.js.map +1 -0
- package/dist/react.d.ts +98 -0
- package/dist/react.js +4 -0
- package/dist/react.js.map +1 -0
- package/dist/runner.d.ts +174 -0
- package/dist/runner.js +3 -0
- package/dist/runner.js.map +1 -0
- package/dist/server.d.ts +658 -0
- package/dist/server.js +995 -0
- package/dist/server.js.map +1 -0
- package/dist/supabase.d.ts +118 -0
- package/dist/supabase.js +3 -0
- package/dist/supabase.js.map +1 -0
- package/dist/types-BilguCPW.d.ts +332 -0
- package/dist/web.d.ts +79 -0
- package/dist/web.js +233 -0
- package/dist/web.js.map +1 -0
- package/functions/genesis-notifications/index.ts +60 -0
- package/functions/genesis-notifications/notifications.ts +18 -0
- package/migrations/0001_genesis_notifications.sql +2057 -0
- package/migrations/tests/0001_genesis_notifications.test.sql +473 -0
- package/package.json +191 -3
- package/src/blocks.ts +42 -0
- package/src/core/index.ts +43 -0
- package/src/core/registry.ts +354 -0
- package/src/core/types.ts +90 -0
- package/src/core/validate.ts +73 -0
- package/src/email/index.tsx +142 -0
- package/src/index.ts +6 -0
- package/src/native/index.tsx +283 -0
- package/src/react/index.tsx +226 -0
- package/src/runner/index.ts +299 -0
- package/src/server/crypto.ts +44 -0
- package/src/server/dns.ts +106 -0
- package/src/server/email-render.ts +90 -0
- package/src/server/errors.ts +61 -0
- package/src/server/expo.ts +133 -0
- package/src/server/handler.ts +127 -0
- package/src/server/index.ts +57 -0
- package/src/server/notify.ts +100 -0
- package/src/server/rate.ts +47 -0
- package/src/server/resend.ts +143 -0
- package/src/server/runner.ts +524 -0
- package/src/server/secret-links.ts +53 -0
- package/src/server/store.ts +150 -0
- package/src/server/unsubscribe.ts +78 -0
- package/src/server/webhook.ts +55 -0
- package/src/supabase/index.ts +210 -0
- package/src/web/index.tsx +428 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
-- @feat:notifications
|
|
2
|
+
-- pgTAP test for 0001_genesis_notifications.sql.
|
|
3
|
+
-- Copy into your app's `supabase/tests/database/` and run `supabase test db`.
|
|
4
|
+
-- It writes only its own users and `gntest.*` types, reads only its own rows,
|
|
5
|
+
-- and rolls everything back. If your app replaced
|
|
6
|
+
-- private.genesis_notifications_admin, it is replaced again inside this
|
|
7
|
+
-- transaction only.
|
|
8
|
+
begin;
|
|
9
|
+
create extension if not exists pgtap with schema extensions;
|
|
10
|
+
select plan(184);
|
|
11
|
+
|
|
12
|
+
-- People: a1 and b2 have logins; c3 is an admin; "outside" has no login.
|
|
13
|
+
insert into auth.users (id, aud, role, email, instance_id)
|
|
14
|
+
select ('00000000-0000-4000-8000-0000000000' || n)::uuid, 'authenticated', 'authenticated', 'gntest-' || n || '@example.test', '00000000-0000-0000-0000-000000000000'
|
|
15
|
+
from unnest(array['a1', 'b2', 'c3']) n;
|
|
16
|
+
|
|
17
|
+
create function pg_temp.u(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-0000000000' || n)::uuid $$;
|
|
18
|
+
create function pg_temp.tok(n text) returns text language sql immutable as $$ select 'ExponentPushToken[gntest-' || n || ']' $$;
|
|
19
|
+
create function pg_temp.sign_in(n text) returns void language plpgsql as $$
|
|
20
|
+
begin
|
|
21
|
+
perform set_config('request.jwt.claims', json_build_object('sub', pg_temp.u(n), 'role', 'authenticated')::text, true);
|
|
22
|
+
perform set_config('request.jwt.claim.sub', pg_temp.u(n)::text, true);
|
|
23
|
+
perform set_config('role', 'authenticated', true);
|
|
24
|
+
end $$;
|
|
25
|
+
create function pg_temp.as_anon() returns void language plpgsql as $$
|
|
26
|
+
begin
|
|
27
|
+
perform set_config('request.jwt.claims', '{"role":"anon"}', true);
|
|
28
|
+
perform set_config('request.jwt.claim.sub', '', true);
|
|
29
|
+
perform set_config('role', 'anon', true);
|
|
30
|
+
end $$;
|
|
31
|
+
grant execute on function pg_temp.u(text), pg_temp.tok(text), pg_temp.sign_in(text), pg_temp.as_anon() to anon, authenticated;
|
|
32
|
+
|
|
33
|
+
-- c3 administers tenant T1 and the app.
|
|
34
|
+
create or replace function private.genesis_notifications_admin(p_user uuid, p_tenant uuid)
|
|
35
|
+
returns boolean language sql stable security definer set search_path = '' as $$
|
|
36
|
+
select p_user = '00000000-0000-4000-8000-0000000000c3'::uuid
|
|
37
|
+
$$;
|
|
38
|
+
|
|
39
|
+
-- Types, as the code would mirror them.
|
|
40
|
+
insert into public.notification_types (key, category, channels, critical, transactional, email_delay_seconds, secret_link, ios_category) values
|
|
41
|
+
('gntest.message', 'gntest-messages', '{"in_app": true, "push": true, "email": true}', false, false, 600, false, 'message'),
|
|
42
|
+
('gntest.receipt', 'gntest-forms', '{"email": true}', false, true, 0, true, null),
|
|
43
|
+
('gntest.security', 'gntest-security', '{"in_app": true, "push": true, "email": true}', true, false, 0, false, null),
|
|
44
|
+
('gntest.news', 'gntest-news', '{"in_app": true, "email": false}', false, false, 0, false, null),
|
|
45
|
+
('gntest.alert', 'gntest-alerts', '{"in_app": true, "push": true}', false, true, 0, false, null);
|
|
46
|
+
|
|
47
|
+
create temp table k (name text primary key, id uuid);
|
|
48
|
+
grant select on k to authenticated, anon;
|
|
49
|
+
create function pg_temp.k(n text) returns uuid language sql stable as $$ select id from k where name = n $$;
|
|
50
|
+
grant execute on function pg_temp.k(text) to authenticated, anon;
|
|
51
|
+
|
|
52
|
+
-- Shape and privileges -----------------------------------------------------------
|
|
53
|
+
select ok((select bool_and(relrowsecurity) from pg_class where oid in (
|
|
54
|
+
'public.notifications'::regclass, 'public.notification_deliveries'::regclass, 'public.notification_types'::regclass,
|
|
55
|
+
'public.notification_senders'::regclass, 'public.notification_tenant_defaults'::regclass, 'public.notification_suppressions'::regclass,
|
|
56
|
+
'public.push_devices'::regclass, 'public.notification_preferences'::regclass, 'public.push_receipts'::regclass, 'public.push_device_moves'::regclass)),
|
|
57
|
+
'row level security is on for every table');
|
|
58
|
+
select has_column('public', 'notification_preferences', 'channels', 'notification_preferences gains channels');
|
|
59
|
+
select has_column('public', 'push_receipts', 'delivery_id', 'push_receipts gains delivery_id');
|
|
60
|
+
select ok(
|
|
61
|
+
not has_function_privilege('authenticated', 'public.genesis_notify(text, jsonb, jsonb, text, text, text, integer, uuid, jsonb, text)', 'execute')
|
|
62
|
+
and not has_function_privilege('anon', 'public.genesis_notify(text, jsonb, jsonb, text, text, text, integer, uuid, jsonb, text)', 'execute')
|
|
63
|
+
and has_function_privilege('service_role', 'public.genesis_notify(text, jsonb, jsonb, text, text, text, integer, uuid, jsonb, text)', 'execute'),
|
|
64
|
+
'only the service role may notify');
|
|
65
|
+
select ok(
|
|
66
|
+
not has_function_privilege('authenticated', 'public.genesis_notifications_claim(text, integer, integer, integer)', 'execute')
|
|
67
|
+
and not has_function_privilege('authenticated', 'public.genesis_notifications_erase(uuid, text, text)', 'execute')
|
|
68
|
+
and not has_function_privilege('authenticated', 'public.genesis_notifications_unsubscribe(uuid, text, text, uuid)', 'execute'),
|
|
69
|
+
'the runner, erasure and unsubscribe are the service role''s');
|
|
70
|
+
select ok(
|
|
71
|
+
(select bool_and(not has_function_privilege('anon', p.oid, 'execute') and not has_function_privilege('public', p.oid, 'execute'))
|
|
72
|
+
from pg_proc p join pg_namespace s on s.oid = p.pronamespace where s.nspname = 'private' and p.proname like 'genesis_notifications%'),
|
|
73
|
+
'no private notifications function is granted to anon or public');
|
|
74
|
+
select ok(not has_column_privilege('authenticated', 'public.notification_deliveries', 'rendered', 'select')
|
|
75
|
+
and not has_column_privilege('authenticated', 'public.notification_deliveries', 'secret_ciphertext', 'select'),
|
|
76
|
+
'nobody signed in can read a rendered email or a secret');
|
|
77
|
+
|
|
78
|
+
-- Devices: knowing a token never takes it -----------------------------------------
|
|
79
|
+
select pg_temp.sign_in('a1');
|
|
80
|
+
select lives_ok($$ select public.genesis_push_register(pg_temp.tok('a1'), 'ios', '1.0', '11111111-1111-4111-8111-111111111111') $$, 'a1 registers its phone with its install id');
|
|
81
|
+
select pg_temp.sign_in('b2');
|
|
82
|
+
select throws_ok($$ select public.genesis_push_register(pg_temp.tok('a1'), 'ios', '1.0') $$, '42501', null, 'b2 cannot take a1''s token without an install id');
|
|
83
|
+
select throws_ok($$ select public.genesis_push_register(pg_temp.tok('a1'), 'ios', '1.0', '22222222-2222-4222-8222-222222222222') $$, '42501', null, '... nor with another phone''s install id');
|
|
84
|
+
reset role;
|
|
85
|
+
select is((select user_id from public.push_devices where token = pg_temp.tok('a1')), pg_temp.u('a1'), '... and the token stays a1''s');
|
|
86
|
+
select is((select count(*) from public.push_device_moves where to_user_id = pg_temp.u('b2')), 0::bigint, '... and nothing moved');
|
|
87
|
+
select pg_temp.sign_in('b2');
|
|
88
|
+
select lives_ok($$ select public.genesis_push_register(pg_temp.tok('a1'), 'ios', '1.0', '11111111-1111-4111-8111-111111111111') $$, 'on the same phone (same install id) b2 may take it over');
|
|
89
|
+
reset role;
|
|
90
|
+
select is((select user_id from public.push_devices where token = pg_temp.tok('a1')), pg_temp.u('b2'), '... and it is b2''s');
|
|
91
|
+
select is((select count(*) from public.push_device_moves where from_user_id = pg_temp.u('a1') and to_user_id = pg_temp.u('b2')), 1::bigint, '... and the move is audited');
|
|
92
|
+
select is((select token_hash from public.push_device_moves where to_user_id = pg_temp.u('b2')), encode(sha256(convert_to(pg_temp.tok('a1'), 'UTF8')), 'hex'), '... by the token''s hash, not the token');
|
|
93
|
+
select pg_temp.sign_in('a1');
|
|
94
|
+
select lives_ok($$ select public.genesis_push_register(pg_temp.tok('a1-phone'), 'ios', '1.0') $$, 'the three-argument register still registers a new token');
|
|
95
|
+
|
|
96
|
+
select pg_temp.as_anon();
|
|
97
|
+
select ok(public.genesis_push_forget(pg_temp.tok('a1-phone')), 'forget works signed out, the token as proof');
|
|
98
|
+
select ok(public.genesis_push_forget(pg_temp.tok('nobody')), '... and answers the same for a token nobody has');
|
|
99
|
+
select ok(public.genesis_push_forget('ExponentPushToken[gntest-%]'), '... and a pattern is just text');
|
|
100
|
+
reset role;
|
|
101
|
+
select is((select count(*) from public.push_devices where token = pg_temp.tok('a1-phone')), 0::bigint, 'the forgotten token is gone');
|
|
102
|
+
select is((select count(*) from public.push_devices where token = pg_temp.tok('a1')), 1::bigint, '... and only that exact token');
|
|
103
|
+
|
|
104
|
+
-- Devices for the push tests below.
|
|
105
|
+
select pg_temp.sign_in('a1');
|
|
106
|
+
select public.genesis_push_register(pg_temp.tok('a1-2'), 'ios', '1.0', '33333333-3333-4333-8333-333333333333');
|
|
107
|
+
reset role;
|
|
108
|
+
|
|
109
|
+
-- Idempotency ---------------------------------------------------------------------
|
|
110
|
+
insert into k select 'm1', notification_id from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('a1')),
|
|
111
|
+
'{"conversation_id": "c1", "preview": "Hi"}', 'push:msg-1', 'c1', null, 3, null, null, null);
|
|
112
|
+
select is((select created from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('a1')), '{"conversation_id": "c1"}', 'push:msg-1')), false,
|
|
113
|
+
'the same key again for the same recipient makes nothing new');
|
|
114
|
+
select is((select count(*) from public.notifications where user_id = pg_temp.u('a1') and idempotency_key = 'push:msg-1'), 1::bigint, '... one row');
|
|
115
|
+
select is((select count(*) from public.notification_deliveries where notification_id = pg_temp.k('m1')), 2::bigint, '... with one push and one email job, not two of each');
|
|
116
|
+
select is((select count(*) from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2')), '{}', 'push:msg-1') where created), 1::bigint,
|
|
117
|
+
'the same key for another recipient is theirs');
|
|
118
|
+
insert into k select 'b1', notification_id from public.genesis_notify('gntest.message', jsonb_build_array(jsonb_build_object('user_id', pg_temp.u('c3'), 'badge', 7)), '{}', 'push:msg-badge', null, null, 1);
|
|
119
|
+
select is((select badge from public.notifications where id = pg_temp.k('b1')), 7, 'a recipient''s own badge wins over the call''s');
|
|
120
|
+
select is((select count(*) from public.genesis_notify('gntest.message', '[{"user_id": "00000000-0000-4000-8000-0000000000ff"}]', '{}', 'push:msg-2')), 0::bigint,
|
|
121
|
+
'a login that no longer exists is skipped');
|
|
122
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.nope', '["00000000-0000-4000-8000-0000000000a1"]', '{}', 'x') $$, '22023', null, 'an unknown type is refused');
|
|
123
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000a1"]', '{}', null) $$, '22023', null, 'an idempotency key is required');
|
|
124
|
+
|
|
125
|
+
-- Feed under row level security ---------------------------------------------------
|
|
126
|
+
select pg_temp.sign_in('a1');
|
|
127
|
+
select is((select count(*) from public.notifications where id = pg_temp.k('m1')), 1::bigint, 'a1 reads its own notification');
|
|
128
|
+
select is((select count(*) from public.notifications where user_id = pg_temp.u('b2')), 0::bigint, '... and none of b2''s');
|
|
129
|
+
select is(public.genesis_notifications_mark(array[pg_temp.k('m1')], 'read'), 1, 'a1 marks its own read');
|
|
130
|
+
select pg_temp.sign_in('b2');
|
|
131
|
+
select is((select count(*) from public.notifications where id = pg_temp.k('m1')), 0::bigint, 'b2 cannot read a1''s notification');
|
|
132
|
+
select is(public.genesis_notifications_mark(array[pg_temp.k('m1')], 'archived'), 0, '... nor mark it');
|
|
133
|
+
select throws_ok($$ select * from public.notification_deliveries $$, '42501', null, 'nobody signed in reads a delivery''s hidden columns (select *)');
|
|
134
|
+
select is((select count(*) from public.notification_deliveries where id is not null), 0::bigint, '... and sees no log row (not an admin)');
|
|
135
|
+
select pg_temp.as_anon();
|
|
136
|
+
select throws_ok($$ select count(*) from public.notifications $$, '42501', null, 'anon cannot read the feed');
|
|
137
|
+
reset role;
|
|
138
|
+
select pg_temp.sign_in('c3');
|
|
139
|
+
select ok((select count(*) from public.notification_deliveries where notification_id = pg_temp.k('m1')) = 2, 'an admin reads the log');
|
|
140
|
+
reset role;
|
|
141
|
+
|
|
142
|
+
-- Secrets never in data -----------------------------------------------------------
|
|
143
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000a1"]', '{"invite_token": "abc"}', 's1') $$,
|
|
144
|
+
'22023', null, 'a key named like a secret is refused');
|
|
145
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000a1"]', '{"x": {"link": "https://app.example/i?token=abc"}}', 's2') $$,
|
|
146
|
+
'22023', null, 'a link carrying a token, however deep, is refused');
|
|
147
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000a1"]', '{}', 's3', null, null, null, null, null, 'plain') $$,
|
|
148
|
+
'22023', null, 'a type without a secret link takes no secret');
|
|
149
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000a1"]', '{"inviteToken": "abc"}', 's4') $$,
|
|
150
|
+
'22023', null, 'a camelCase secret key is refused too');
|
|
151
|
+
select lives_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000a1"]', '{"token_count": 3}', 's5') $$,
|
|
152
|
+
'... but not a key that only starts with one');
|
|
153
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.receipt', '[{"email": "x1@example.test"}, {"email": "x2@example.test"}]', '{}', 's6', null, null, null, null, null, 'one-time') $$,
|
|
154
|
+
'22023', null, 'a one-time secret goes to exactly one recipient, never copied to another');
|
|
155
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["not-a-uuid"]', '{}', 's7') $$,
|
|
156
|
+
'22023', 'genesis_notify: a recipient is not a user id', 'a malformed user id is refused without echoing it');
|
|
157
|
+
insert into k select 'r1', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "outside@example.test", "name": "Out Side", "ref": "people:9"}]',
|
|
158
|
+
'{"response_id": "resp-1"}', 'receipt:resp-1', null, null, null, null, null, 'plain-secret-value');
|
|
159
|
+
select ok(not exists (select 1 from public.notifications where id = pg_temp.k('r1') and data::text like '%plain-secret-value%'), 'the secret is not in data');
|
|
160
|
+
select ok((select secret_ciphertext is not null and position('plain-secret-value' in encode(secret_ciphertext, 'escape')) = 0 from public.notification_deliveries where notification_id = pg_temp.k('r1')),
|
|
161
|
+
'... it travels encrypted on the email job');
|
|
162
|
+
create temp table claimed as select * from public.genesis_notifications_claim('email', 50);
|
|
163
|
+
select is((select secret from claimed where notification_id = pg_temp.k('r1')), 'plain-secret-value', 'the runner gets it back when it claims the email');
|
|
164
|
+
select is((select email from claimed where notification_id = pg_temp.k('r1')), 'outside@example.test', '... with the address of someone with no login');
|
|
165
|
+
select ok(public.genesis_notifications_settle((select id from claimed where notification_id = pg_temp.k('r1')), (select claim_id from claimed where notification_id = pg_temp.k('r1')),
|
|
166
|
+
'{"status": "sent", "provider_id": "re_1"}'), 'settling with the claim id records it');
|
|
167
|
+
select ok((select secret_ciphertext is null and status = 'sent' from public.notification_deliveries where notification_id = pg_temp.k('r1')), '... and clears the secret');
|
|
168
|
+
select ok(not public.genesis_notifications_settle((select id from claimed where notification_id = pg_temp.k('r1')), gen_random_uuid(), '{"status": "failed"}'),
|
|
169
|
+
'a stale claim settles nothing');
|
|
170
|
+
|
|
171
|
+
-- Preferences ----------------------------------------------------------------------
|
|
172
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), null, 'gntest.news', 'email'), false, 'an opt-in channel is off by default');
|
|
173
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), null, 'gntest.news', 'in_app'), true, '... and a default one on');
|
|
174
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), null, 'gntest.receipt', 'push'), false, 'a channel the type doesn''t offer is never on');
|
|
175
|
+
insert into public.notification_tenant_defaults (tenant_id, category, channel, enabled) values ('00000000-0000-4000-8000-00000000aaaa', 'gntest-news', 'email', true);
|
|
176
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), '00000000-0000-4000-8000-00000000aaaa', 'gntest.news', 'email'), true, 'a tenant default turns it on for its people');
|
|
177
|
+
select pg_temp.sign_in('b2');
|
|
178
|
+
select lives_ok($$ select public.genesis_notifications_set_preference('gntest-news', 'email', false) $$, 'b2 turns it off');
|
|
179
|
+
select lives_ok($$ select public.genesis_notifications_set_preference('gntest-messages', 'push', false) $$, '... and message pushes');
|
|
180
|
+
select is((select channels from public.notification_preferences where user_id = pg_temp.u('b2')), '{"gntest-news": {"email": false}, "gntest-messages": {"push": false}}'::jsonb,
|
|
181
|
+
'each choice merges into the row');
|
|
182
|
+
select throws_ok($$ select public.genesis_notifications_set_preference('gntest-security', 'email', false) $$, '22023', null, 'a critical category can''t be turned off');
|
|
183
|
+
select throws_ok($$ select public.genesis_notifications_set_preference('gntest-forms', 'push', true) $$, '42704', null, 'nor a channel no type offers turned on (its own code)');
|
|
184
|
+
reset role;
|
|
185
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), '00000000-0000-4000-8000-00000000aaaa', 'gntest.news', 'email'), false, 'the person''s choice beats the tenant''s default');
|
|
186
|
+
update public.notification_preferences set channels = channels || '{"gntest-security": {"email": false}}' where user_id = pg_temp.u('b2');
|
|
187
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), null, 'gntest.security', 'email'), true, 'a critical type ignores a choice that slipped in');
|
|
188
|
+
insert into public.notification_preferences (user_id, categories) values (pg_temp.u('c3'), '{"gntest-messages": false}');
|
|
189
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('c3'), null, 'gntest.message', 'push'), false, '0005''s category switch still turns push off');
|
|
190
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('c3'), null, 'gntest.message', 'in_app'), true, '... but not the feed');
|
|
191
|
+
|
|
192
|
+
-- Quiet hours across daylight saving (America/New_York) ------------------------------
|
|
193
|
+
update public.notification_preferences set quiet_hours_enabled = true, quiet_hours_start = '22:00', quiet_hours_end = '02:30', time_zone = 'America/New_York'
|
|
194
|
+
where user_id = pg_temp.u('c3');
|
|
195
|
+
select is(private.genesis_notifications_quiet_until(pg_temp.u('c3'), '2026-03-08 06:30+00'), '2026-03-08 07:00+00'::timestamptz,
|
|
196
|
+
'an end time the spring-forward change skips ends quiet hours at the change');
|
|
197
|
+
select is(private.genesis_notifications_quiet_until(pg_temp.u('c3'), '2026-03-07 04:00+00'), '2026-03-07 07:30+00'::timestamptz, '... and at 02:30 the night before');
|
|
198
|
+
update public.notification_preferences set quiet_hours_start = '01:30', quiet_hours_end = '06:00' where user_id = pg_temp.u('c3');
|
|
199
|
+
select is(private.genesis_notifications_quiet_until(pg_temp.u('c3'), '2026-11-01 05:45+00'), '2026-11-01 11:00+00'::timestamptz, 'fall back: quiet from the first 01:30');
|
|
200
|
+
select is(private.genesis_notifications_quiet_until(pg_temp.u('c3'), '2026-11-01 06:45+00'), '2026-11-01 11:00+00'::timestamptz, '... the repeated hour doesn''t restart it');
|
|
201
|
+
select is(private.genesis_notifications_quiet_until(pg_temp.u('c3'), '2026-11-01 11:00+00'), null, '... and the end is exclusive');
|
|
202
|
+
update public.notification_preferences set quiet_hours_start = '06:00' where user_id = pg_temp.u('c3');
|
|
203
|
+
select is(private.genesis_notifications_quiet_until(pg_temp.u('c3'), '2026-11-01 10:59+00'), null, 'a start equal to the end is no quiet hours');
|
|
204
|
+
|
|
205
|
+
-- Push during quiet hours waits; a transactional one doesn't ----------------------------
|
|
206
|
+
-- a1 is quiet all day long, in UTC.
|
|
207
|
+
insert into public.notification_preferences (user_id, quiet_hours_enabled, quiet_hours_start, quiet_hours_end, time_zone)
|
|
208
|
+
values (pg_temp.u('a1'), true, '00:00', '23:59', 'UTC')
|
|
209
|
+
on conflict (user_id) do update set quiet_hours_enabled = true, quiet_hours_start = '00:00', quiet_hours_end = '23:59', time_zone = 'UTC';
|
|
210
|
+
insert into k select 'q1', notification_id from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('a1')), '{}', 'push:msg-q');
|
|
211
|
+
insert into k select 'q2', notification_id from public.genesis_notify('gntest.alert', jsonb_build_array(pg_temp.u('a1')), '{}', 'alert-1');
|
|
212
|
+
update public.notification_deliveries set send_after = now() - interval '1 minute' where notification_id in (pg_temp.k('q1'), pg_temp.k('q2'));
|
|
213
|
+
create temp table pushes as select * from public.genesis_notifications_claim('push', 50);
|
|
214
|
+
select ok(not exists (select 1 from pushes where notification_id = pg_temp.k('q1')), 'a push in quiet hours is not handed out');
|
|
215
|
+
select ok((select status = 'pending' and send_after > now() and attempts = 0 from public.notification_deliveries where notification_id = pg_temp.k('q1') and channel = 'push'),
|
|
216
|
+
'... it waits until they end, with no attempt used');
|
|
217
|
+
select ok(exists (select 1 from pushes where notification_id = pg_temp.k('q2') and jsonb_array_length(devices) = 1), 'a transactional push goes through quiet hours, to the person''s devices');
|
|
218
|
+
select is((select d.skip_reason from public.notification_deliveries d join public.notifications n on n.id = d.notification_id
|
|
219
|
+
where n.user_id = pg_temp.u('b2') and n.idempotency_key = 'push:msg-1' and d.channel = 'push'), 'preference',
|
|
220
|
+
'a push queued before the person turned the channel off is skipped at send');
|
|
221
|
+
|
|
222
|
+
-- Email: delay, then skip if read; suppressions; transactional bypass -----------------------
|
|
223
|
+
select ok((select send_after > now() + interval '9 minutes' from public.notification_deliveries where notification_id = pg_temp.k('m1') and channel = 'email'),
|
|
224
|
+
'a message email waits its delay');
|
|
225
|
+
update public.notification_deliveries set send_after = now() - interval '1 second' where notification_id = pg_temp.k('m1') and channel = 'email';
|
|
226
|
+
select ok(not exists (select 1 from public.genesis_notifications_claim('email', 50) c where c.notification_id = pg_temp.k('m1')), 'a1 read it in the app, so no email');
|
|
227
|
+
select is((select skip_reason from public.notification_deliveries where notification_id = pg_temp.k('m1') and channel = 'email'), 'read_in_app', '... recorded as read_in_app');
|
|
228
|
+
|
|
229
|
+
insert into public.notification_suppressions (email_hash, category, reason) values (private.genesis_notifications_email_hash('GNTEST-b2@example.test '), '*', 'complaint');
|
|
230
|
+
insert into k select 'n1', notification_id from public.genesis_notify('gntest.security', jsonb_build_array(pg_temp.u('b2')), '{}', 'sec-1');
|
|
231
|
+
insert into k select 'n2', notification_id from public.genesis_notify('gntest.receipt', jsonb_build_array(pg_temp.u('b2')), '{}', 'rcpt-b2');
|
|
232
|
+
create temp table mails as select * from public.genesis_notifications_claim('email', 50);
|
|
233
|
+
select is((select skip_reason from public.notification_deliveries where notification_id = pg_temp.k('n1') and channel = 'email'), 'suppressed', 'a complaint stops non-transactional mail');
|
|
234
|
+
select ok(exists (select 1 from mails where notification_id = pg_temp.k('n2') and transactional), 'a transactional email still goes to a complainer');
|
|
235
|
+
select ok((select send_after <= now() from public.notification_deliveries where notification_id = pg_temp.k('n2')), '... and is never delayed');
|
|
236
|
+
select ok(public.genesis_notifications_unsubscribe(null, private.genesis_notifications_email_hash('outside@example.test'), 'gntest-forms'), 'someone with no login unsubscribes from a category');
|
|
237
|
+
select ok(public.genesis_notifications_unsubscribe(pg_temp.u('a1'), null, 'gntest-messages'), 'a login unsubscribes through their preferences');
|
|
238
|
+
select is((select tenant_channels -> '00000000-0000-0000-0000-000000000000' -> 'gntest-messages' ->> 'email' from public.notification_preferences where user_id = pg_temp.u('a1')), 'false',
|
|
239
|
+
'... the category''s email is off for mail sent for no tenant');
|
|
240
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('a1'), null, 'gntest.message', 'email'), false, '... so no such email goes');
|
|
241
|
+
select ok(public.genesis_notifications_email_event('re_1', 'bounced', true), 'a hard bounce is recorded');
|
|
242
|
+
select ok(exists (select 1 from public.notification_suppressions where email_hash = private.genesis_notifications_email_hash('outside@example.test') and category = '*' and reason = 'bounce'),
|
|
243
|
+
'... and suppresses the address');
|
|
244
|
+
|
|
245
|
+
-- Collapse ---------------------------------------------------------------------------
|
|
246
|
+
insert into k select 'c1', notification_id from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2')), '{}', 'push:c-1', 't1', 'conv-1');
|
|
247
|
+
insert into k select 'c2', notification_id from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2')), '{}', 'push:c-2', 't1', 'conv-1');
|
|
248
|
+
select is((select status from public.notification_deliveries where notification_id = pg_temp.k('c1') and channel = 'email'), 'cancelled', 'a newer message with the same collapse key cancels the older unsent email');
|
|
249
|
+
select ok((select archived_at is not null from public.notifications where id = pg_temp.k('c1')), '... and archives the older feed row');
|
|
250
|
+
select ok((select archived_at is null from public.notifications where id = pg_temp.k('c2')), '... leaving the newer one');
|
|
251
|
+
|
|
252
|
+
-- Type mirror --------------------------------------------------------------------------
|
|
253
|
+
select is(public.genesis_notifications_sync_types((select jsonb_agg(to_jsonb(t) - 'retired_at' - 'updated_at') from public.notification_types t where t.key <> 'gntest.news')),
|
|
254
|
+
(select count(*)::integer from public.notification_types where key <> 'gntest.news'), 'syncing writes every type in the list');
|
|
255
|
+
select ok((select retired_at is not null from public.notification_types where key = 'gntest.news'), '... retires one left out (never deletes it)');
|
|
256
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.news', '["00000000-0000-4000-8000-0000000000a1"]', '{}', 'n-x') $$, '22023', null, '... and a retired type takes no new notifications');
|
|
257
|
+
|
|
258
|
+
-- Admin --------------------------------------------------------------------------------
|
|
259
|
+
select pg_temp.sign_in('c3');
|
|
260
|
+
select ok(public.genesis_notifications_send_test() is not null, 'an admin sends a test to themself');
|
|
261
|
+
select pg_temp.sign_in('b2');
|
|
262
|
+
select throws_ok($$ select public.genesis_notifications_send_test() $$, '42501', null, 'someone else may not');
|
|
263
|
+
select throws_ok($$ select public.genesis_notifications_set_tenant_default('00000000-0000-4000-8000-00000000aaaa', 'gntest-news', 'email', false) $$, '42501', null, '... nor change a tenant''s defaults');
|
|
264
|
+
reset role;
|
|
265
|
+
|
|
266
|
+
-- Transactional email ignores unsubscribes ------------------------------------------------
|
|
267
|
+
update public.notification_preferences set channels = channels || '{"gntest-forms": {"email": false}}' where user_id = pg_temp.u('b2');
|
|
268
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), null, 'gntest.receipt', 'email'), true, 'an unsubscribe never stops a transactional email');
|
|
269
|
+
|
|
270
|
+
-- Cancel ------------------------------------------------------------------------------------
|
|
271
|
+
create temp table release42 as select * from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2'), pg_temp.u('c3')), '{}', 'release:42');
|
|
272
|
+
update public.notification_preferences set categories = '{}' where user_id = pg_temp.u('c3');
|
|
273
|
+
insert into k select 'x3', notification_id from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('c3')), '{}', 'release:43');
|
|
274
|
+
update public.notification_deliveries set status = 'sent', sent_at = now()
|
|
275
|
+
where notification_id = pg_temp.k('x3') and channel = 'push';
|
|
276
|
+
select is((select cancelled from public.genesis_notifications_cancel('gntest.message', 'release:42', null, jsonb_build_array(pg_temp.u('b2')))), 1,
|
|
277
|
+
'cancel stops the pending email of only the recipient named');
|
|
278
|
+
select is((select status from public.notification_deliveries d join public.notifications n on n.id = d.notification_id
|
|
279
|
+
where n.idempotency_key = 'release:42' and n.user_id = pg_temp.u('b2') and d.channel = 'email'), 'cancelled', '... marked cancelled');
|
|
280
|
+
select is((select count(*) from public.notification_deliveries d join public.notifications n on n.id = d.notification_id
|
|
281
|
+
where n.idempotency_key = 'release:42' and n.user_id = pg_temp.u('c3') and d.status = 'pending'), 0::bigint,
|
|
282
|
+
'... (c3 had message email off, so nothing was queued for them)');
|
|
283
|
+
select is((select archived_at from public.notifications where idempotency_key = 'release:42' and user_id = pg_temp.u('b2')), null, '... and leaves the feed row without archive');
|
|
284
|
+
select is((select row(cancelled, archived)::text from public.genesis_notifications_cancel('gntest.message', 'release:43', null, null, true)), '(1,1)',
|
|
285
|
+
'cancel with archive: the pending email cancelled and the feed row archived');
|
|
286
|
+
select is((select status from public.notification_deliveries where notification_id = pg_temp.k('x3') and channel = 'push'), 'sent', 'a job already sent is left alone');
|
|
287
|
+
update public.notification_deliveries set send_after = now() - interval '1 day' where status = 'cancelled';
|
|
288
|
+
select ok(not exists (select 1 from public.genesis_notifications_claim('email', 500) c join public.notification_deliveries d on d.id = c.id
|
|
289
|
+
join public.notifications n on n.id = d.notification_id where n.idempotency_key in ('release:42', 'release:43')),
|
|
290
|
+
'a cancelled job is never claimed');
|
|
291
|
+
select throws_ok($$ select * from public.genesis_notifications_cancel('gntest.message', null) $$, '22023', null, 'cancel needs an idempotency key');
|
|
292
|
+
select ok(not has_function_privilege('authenticated', 'public.genesis_notifications_cancel(text, text, uuid, jsonb, boolean)', 'execute'), '... and is the service role''s');
|
|
293
|
+
|
|
294
|
+
-- A job whose lease keeps running out fails --------------------------------------------------
|
|
295
|
+
update public.notification_deliveries set status = 'sending', lease_until = now() - interval '1 minute', attempts = 8, send_after = now() - interval '1 hour'
|
|
296
|
+
where notification_id = pg_temp.k('x3') and channel = 'email';
|
|
297
|
+
select ok(not exists (select 1 from public.genesis_notifications_claim('email', 500) c where c.notification_id = pg_temp.k('x3')), 'a job that lost its lease 8 times isn''t handed out again');
|
|
298
|
+
select is((select status || ':' || error from public.notification_deliveries where notification_id = pg_temp.k('x3') and channel = 'email'), 'failed:lease_expired', '... it fails as lease_expired');
|
|
299
|
+
|
|
300
|
+
-- 0005's category switch becomes channel choices on the first change ---------------------------
|
|
301
|
+
update public.notification_preferences set categories = '{"gntest-messages": false}', channels = '{}' where user_id = pg_temp.u('c3');
|
|
302
|
+
select pg_temp.sign_in('c3');
|
|
303
|
+
select is(public.genesis_notifications_set_preference('gntest-messages', 'push', true), '{"gntest-messages": {"push": true, "email": false}}'::jsonb,
|
|
304
|
+
'turning push back on keeps email off, as the old switch had it');
|
|
305
|
+
reset role;
|
|
306
|
+
select is((select categories from public.notification_preferences where user_id = pg_temp.u('c3')), '{}'::jsonb, '... and the old switch is gone');
|
|
307
|
+
|
|
308
|
+
-- Erasure ------------------------------------------------------------------------------
|
|
309
|
+
select is(public.genesis_notifications_erase(null, 'OUTSIDE@example.test', null), 1, 'erasing someone with no login, by address');
|
|
310
|
+
select is((select count(*) from public.notifications where lower(email) = 'outside@example.test' or person_ref = 'people:9'), 0::bigint, '... leaves none of their notifications');
|
|
311
|
+
select ok(public.genesis_notifications_erase(pg_temp.u('a1')) >= 1, 'erasing a login');
|
|
312
|
+
select is((select count(*) from public.notifications where user_id = pg_temp.u('a1')), 0::bigint, '... deletes their notifications');
|
|
313
|
+
select is((select count(*) from public.notification_deliveries d where not exists (select 1 from public.notifications n where n.id = d.notification_id)), 0::bigint, '... and every send job with them');
|
|
314
|
+
select is((select count(*) from public.push_devices where user_id = pg_temp.u('a1')), 0::bigint, '... their devices');
|
|
315
|
+
select is((select count(*) from public.notification_preferences where user_id = pg_temp.u('a1')), 0::bigint, '... and their preferences');
|
|
316
|
+
select is((select count(*) from public.push_device_moves where from_user_id = pg_temp.u('a1') or to_user_id = pg_temp.u('a1')), 0::bigint, '... and their device moves');
|
|
317
|
+
select ok((public.genesis_notifications_prune(interval '1 hour', interval '0'))->>'deliveries' is not null, 'prune runs and reports what it removed');
|
|
318
|
+
|
|
319
|
+
-- Review fixes ------------------------------------------------------------------------------
|
|
320
|
+
create function pg_temp.t(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-00000000' || repeat(n, 4))::uuid $$;
|
|
321
|
+
|
|
322
|
+
-- Sender domains: one tenant per verified domain, proved with its own token.
|
|
323
|
+
select pg_temp.sign_in('c3');
|
|
324
|
+
select lives_ok($$ select public.genesis_notifications_set_sender(pg_temp.t('a'), 'Tenant A', null, 'mail.gntest.example') $$, 'tenant A sets its own domain');
|
|
325
|
+
select lives_ok($$ select public.genesis_notifications_set_sender(pg_temp.t('b'), 'Tenant B', null, 'mail.gntest.example') $$, '... and tenant B may name the same one while neither has verified it');
|
|
326
|
+
reset role;
|
|
327
|
+
select ok((select bool_and(domain_token ~ '^[0-9a-f]{32}$') from public.notification_senders where tenant_id in (pg_temp.t('a'), pg_temp.t('b'))), 'each sender gets an ownership token for the TXT record');
|
|
328
|
+
select ok((select count(distinct domain_token) = 2 from public.notification_senders where tenant_id in (pg_temp.t('a'), pg_temp.t('b'))), '... a token of its own');
|
|
329
|
+
select ok(not public.genesis_notifications_set_sender_dns(pg_temp.t('a'), 'mail.gntest.example', repeat('0', 32), 'verified', '{"ownership": true}'), 'a check made with another token changes nothing');
|
|
330
|
+
select ok(public.genesis_notifications_set_sender_dns(pg_temp.t('a'), 'mail.gntest.example', (select domain_token from public.notification_senders where tenant_id = pg_temp.t('a')), 'verified', '{"ownership": true}'), 'tenant A verifies it');
|
|
331
|
+
select throws_ok($$ select public.genesis_notifications_set_sender_dns(pg_temp.t('b'), 'mail.gntest.example', (select domain_token from public.notification_senders where tenant_id = pg_temp.t('b')), 'verified', '{"ownership": true}') $$, '23505', null,
|
|
332
|
+
'tenant B can never verify a domain tenant A has verified');
|
|
333
|
+
select is((select domain_status from public.notification_senders where tenant_id = pg_temp.t('b')), 'pending', '... and stays unverified');
|
|
334
|
+
select ok(not public.genesis_notifications_set_sender_dns(pg_temp.t('a'), 'other.gntest.example', (select domain_token from public.notification_senders where tenant_id = pg_temp.t('a')), 'failed', '{}'), 'a check of a domain the tenant no longer has changes nothing');
|
|
335
|
+
select pg_temp.sign_in('c3');
|
|
336
|
+
select throws_ok($$ select public.genesis_notifications_set_sender(pg_temp.t('c'), 'Tenant C', null, 'MAIL.gntest.example') $$, '23505', null, 'a third tenant may not even name a verified domain');
|
|
337
|
+
select lives_ok($$ select public.genesis_notifications_set_sender(pg_temp.t('b'), 'Tenant B', null, 'b.gntest.example') $$, 'tenant B moves to another domain');
|
|
338
|
+
reset role;
|
|
339
|
+
select ok((select domain_token is not null and domain_status = 'pending' from public.notification_senders where tenant_id = pg_temp.t('b')), '... pending again, with a fresh token');
|
|
340
|
+
|
|
341
|
+
-- Idempotency is per type and tenant.
|
|
342
|
+
select is((select count(*) from (
|
|
343
|
+
select * from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2')), '{}', 'tenant-key', null, null, null, pg_temp.t('a'))
|
|
344
|
+
union all
|
|
345
|
+
select * from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2')), '{}', 'tenant-key', null, null, null, pg_temp.t('b'))
|
|
346
|
+
) x where created), 2::bigint, 'the same key for tenants A and B gives two notifications');
|
|
347
|
+
select is((select created from public.genesis_notify('gntest.message', jsonb_build_array(pg_temp.u('b2')), '{}', 'tenant-key', null, null, null, pg_temp.t('a'))), false,
|
|
348
|
+
'... and the key again for tenant A finds tenant A''s');
|
|
349
|
+
select is((select created from public.genesis_notify('gntest.alert', jsonb_build_array(pg_temp.u('b2')), '{}', 'tenant-key', null, null, null, pg_temp.t('a'))), true,
|
|
350
|
+
'... while another type with the key is a notification of its own');
|
|
351
|
+
select is((select cancelled from public.genesis_notifications_cancel('gntest.message', 'tenant-key', pg_temp.t('a'))), 1, 'cancel for tenant A cancels its pending email');
|
|
352
|
+
select is((select d.status from public.notification_deliveries d join public.notifications n on n.id = d.notification_id
|
|
353
|
+
where n.idempotency_key = 'tenant-key' and n.tenant_id = pg_temp.t('b') and d.channel = 'email'), 'pending', '... and never tenant B''s');
|
|
354
|
+
select is((select cancelled from public.genesis_notifications_cancel('gntest.message', 'tenant-key')), 0, '... nor does a cancel with no tenant');
|
|
355
|
+
|
|
356
|
+
-- A cancel during a send sticks.
|
|
357
|
+
insert into k select 'cm', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "cancel-mid@example.test"}]', '{}', 'cancel-mid');
|
|
358
|
+
insert into k select 'cl', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "cancel-lease@example.test"}]', '{}', 'cancel-lease');
|
|
359
|
+
create temp table cmc as select * from public.genesis_notifications_claim('email', 500);
|
|
360
|
+
select is((select count(*) from cmc where notification_id in (pg_temp.k('cm'), pg_temp.k('cl'))), 2::bigint, 'claimed: both are being sent');
|
|
361
|
+
select is((select cancelled from public.genesis_notifications_cancel('gntest.receipt', 'cancel-mid')), 1, 'a cancel while one is being sent counts it');
|
|
362
|
+
select ok(public.genesis_notifications_settle((select id from cmc where notification_id = pg_temp.k('cm')), (select claim_id from cmc where notification_id = pg_temp.k('cm')),
|
|
363
|
+
'{"status": "retry", "error": "Resend answered HTTP 500", "counted": true}'), 'the runner settles a retry');
|
|
364
|
+
select is((select status || ':' || skip_reason from public.notification_deliveries where notification_id = pg_temp.k('cm')), 'cancelled:cancelled', '... and it ends cancelled, not back in the queue');
|
|
365
|
+
select is((select cancelled from public.genesis_notifications_cancel('gntest.receipt', 'cancel-lease')), 1, 'cancel the other mid-send');
|
|
366
|
+
update public.notification_deliveries set lease_until = now() - interval '1 minute', send_after = now() - interval '1 hour' where notification_id in (pg_temp.k('cm'), pg_temp.k('cl'));
|
|
367
|
+
select ok(not exists (select 1 from public.genesis_notifications_claim('email', 500) c where c.notification_id in (pg_temp.k('cm'), pg_temp.k('cl'))),
|
|
368
|
+
'claim -> cancel -> settle retry (or a lost lease) -> never claimed again');
|
|
369
|
+
select is((select status from public.notification_deliveries where notification_id = pg_temp.k('cl')), 'cancelled', '... a lost lease ends cancelled too');
|
|
370
|
+
|
|
371
|
+
-- One secret that won't decrypt fails alone.
|
|
372
|
+
insert into k select 'sb', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "bad-secret@example.test"}]', '{}', 'bad-secret', null, null, null, null, null, 'unreadable');
|
|
373
|
+
insert into k select 'sg', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "good-secret@example.test"}]', '{}', 'good-secret', null, null, null, null, null, 'readable');
|
|
374
|
+
update public.notification_deliveries set secret_ciphertext = '\xc30d04070302deadbeef'::bytea where notification_id = pg_temp.k('sb');
|
|
375
|
+
create temp table sbc as select * from public.genesis_notifications_claim('email', 500);
|
|
376
|
+
select is((select secret from sbc where notification_id = pg_temp.k('sg')), 'readable', 'a corrupt secret on one email never blocks another''s');
|
|
377
|
+
select is((select status || ':' || error from public.notification_deliveries where notification_id = pg_temp.k('sb')), 'failed:secret_unreadable', '... it fails as secret_unreadable');
|
|
378
|
+
select ok((select secret_ciphertext is null from public.notification_deliveries where notification_id = pg_temp.k('sb')), '... and drops the ciphertext');
|
|
379
|
+
|
|
380
|
+
-- The provider's id is kept before settle, so an early webhook finds the job.
|
|
381
|
+
select ok(public.genesis_notifications_record_provider_id((select id from sbc where notification_id = pg_temp.k('sg')), (select claim_id from sbc where notification_id = pg_temp.k('sg')), 're_early'),
|
|
382
|
+
'the provider id is kept as soon as the send returns');
|
|
383
|
+
select ok(public.genesis_notifications_email_event('re_early', 'delivered'), 'a webhook that comes before settle finds the job');
|
|
384
|
+
select is((select status from public.notification_deliveries where notification_id = pg_temp.k('sg')), 'delivered', '... and settles it');
|
|
385
|
+
select ok(not public.genesis_notifications_settle((select id from sbc where notification_id = pg_temp.k('sg')), (select claim_id from sbc where notification_id = pg_temp.k('sg')), '{"status": "sent"}'),
|
|
386
|
+
'... so the runner''s late settle is stale and changes nothing');
|
|
387
|
+
select ok(not has_function_privilege('authenticated', 'public.genesis_notifications_record_provider_id(uuid, uuid, text)', 'execute'), '... and it is the service role''s');
|
|
388
|
+
|
|
389
|
+
-- Unsubscribe is for the tenant the email came from.
|
|
390
|
+
select ok(public.genesis_notifications_unsubscribe(null, private.genesis_notifications_email_hash('Tenant-Out@example.test'), 'gntest-messages', pg_temp.t('a')),
|
|
391
|
+
'someone with no login unsubscribes from tenant A''s mail, by the address''s hash');
|
|
392
|
+
insert into k select 'ua', notification_id from public.genesis_notify('gntest.message', '[{"email": "tenant-out@example.test"}]', '{}', 'unsub-a', null, null, null, pg_temp.t('a'));
|
|
393
|
+
insert into k select 'ub', notification_id from public.genesis_notify('gntest.message', '[{"email": "tenant-out@example.test"}]', '{}', 'unsub-b', null, null, null, pg_temp.t('b'));
|
|
394
|
+
create temp table uc as select * from public.genesis_notifications_claim('email', 500);
|
|
395
|
+
select is((select skip_reason from public.notification_deliveries where notification_id = pg_temp.k('ua')), 'suppressed', '... tenant A''s mail stops');
|
|
396
|
+
select ok(exists (select 1 from uc where notification_id = pg_temp.k('ub')), '... tenant B''s doesn''t');
|
|
397
|
+
select ok(public.genesis_notifications_unsubscribe(pg_temp.u('b2'), null, 'gntest-messages', pg_temp.t('a')), 'a login unsubscribes from tenant A''s mail');
|
|
398
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), pg_temp.t('a'), 'gntest.message', 'email'), false, '... off for tenant A');
|
|
399
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), pg_temp.t('b'), 'gntest.message', 'email'), true, '... still on for tenant B');
|
|
400
|
+
select pg_temp.sign_in('b2');
|
|
401
|
+
select lives_ok($$ select public.genesis_notifications_set_preference('gntest-messages', 'email', true) $$, 'turning it on in Settings');
|
|
402
|
+
reset role;
|
|
403
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('b2'), pg_temp.t('a'), 'gntest.message', 'email'), true, '... turns it on for tenant A again');
|
|
404
|
+
|
|
405
|
+
-- A token from before install ids is adopted by the first register that brings one.
|
|
406
|
+
-- As 0005 left it (the migration marks those rows adoptable).
|
|
407
|
+
insert into public.push_devices (user_id, token, platform, adoptable) values (pg_temp.u('c3'), pg_temp.tok('legacy'), 'ios', true);
|
|
408
|
+
select pg_temp.sign_in('c3');
|
|
409
|
+
select lives_ok($$ select public.genesis_push_register(pg_temp.tok('c3-new'), 'ios', '1.0') $$, 'a token registered since, with no install id');
|
|
410
|
+
select pg_temp.sign_in('b2');
|
|
411
|
+
select throws_ok($$ select public.genesis_push_register(pg_temp.tok('c3-new'), 'ios', '1.0', gen_random_uuid()) $$, '42501', null,
|
|
412
|
+
'... can never be taken by inventing an install id: only 0005''s rows are adoptable');
|
|
413
|
+
reset role;
|
|
414
|
+
select pg_temp.sign_in('b2');
|
|
415
|
+
select lives_ok($$ select public.genesis_push_register(pg_temp.tok('legacy'), 'ios', '1.0', '44444444-4444-4444-8444-444444444444') $$,
|
|
416
|
+
'a token 0005 left with no install id is adopted by the first register that brings one');
|
|
417
|
+
reset role;
|
|
418
|
+
select is((select user_id::text || ':' || installation_id::text from public.push_devices where token = pg_temp.tok('legacy')),
|
|
419
|
+
pg_temp.u('b2')::text || ':44444444-4444-4444-8444-444444444444', '... it is b2''s, with that install id');
|
|
420
|
+
select is((select count(*) from public.push_device_moves where from_user_id = pg_temp.u('c3') and to_user_id = pg_temp.u('b2')), 1::bigint, '... and the move is audited');
|
|
421
|
+
select pg_temp.sign_in('c3');
|
|
422
|
+
select throws_ok($$ select public.genesis_push_register(pg_temp.tok('legacy'), 'ios', '1.0', '55555555-5555-4555-8555-555555555555') $$, '42501', null,
|
|
423
|
+
'... after which only that phone may move it');
|
|
424
|
+
reset role;
|
|
425
|
+
|
|
426
|
+
-- An unsubscribe from mail sent for no tenant holds for that mail only.
|
|
427
|
+
select ok(public.genesis_notifications_unsubscribe(null, private.genesis_notifications_email_hash('no-tenant@example.test'), 'gntest-messages'),
|
|
428
|
+
'someone with no login unsubscribes from mail sent for no tenant');
|
|
429
|
+
insert into k select 'nt0', notification_id from public.genesis_notify('gntest.message', '[{"email": "no-tenant@example.test"}]', '{}', 'nt-0');
|
|
430
|
+
insert into k select 'nta', notification_id from public.genesis_notify('gntest.message', '[{"email": "no-tenant@example.test"}]', '{}', 'nt-a', null, null, null, pg_temp.t('a'));
|
|
431
|
+
create temp table ntc as select * from public.genesis_notifications_claim('email', 500);
|
|
432
|
+
select is((select skip_reason from public.notification_deliveries where notification_id = pg_temp.k('nt0')), 'suppressed', '... that mail stops');
|
|
433
|
+
select ok(exists (select 1 from ntc where notification_id = pg_temp.k('nta')), '... but never a tenant''s');
|
|
434
|
+
select ok(public.genesis_notifications_unsubscribe(pg_temp.u('c3'), null, 'gntest-alerts'), 'a login unsubscribes from mail sent for no tenant');
|
|
435
|
+
update public.notification_types set channels = '{"in_app": true, "push": true, "email": true}', transactional = false where key = 'gntest.alert';
|
|
436
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('c3'), null, 'gntest.alert', 'email'), false, '... off for no tenant');
|
|
437
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('c3'), pg_temp.t('a'), 'gntest.alert', 'email'), true, '... still on for tenant A');
|
|
438
|
+
|
|
439
|
+
-- A job collapsed while it was being sent ends collapsed.
|
|
440
|
+
insert into k select 'co1', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "collapse-mid@example.test"}]', '{}', 'co-1', null, 'co-key');
|
|
441
|
+
create temp table coc as select * from public.genesis_notifications_claim('email', 500);
|
|
442
|
+
insert into k select 'co2', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "collapse-mid@example.test"}]', '{}', 'co-2', null, 'co-key');
|
|
443
|
+
select ok(public.genesis_notifications_settle((select id from coc where notification_id = pg_temp.k('co1')), (select claim_id from coc where notification_id = pg_temp.k('co1')),
|
|
444
|
+
'{"status": "retry", "error": "x", "counted": true}'), 'a retry of a job collapsed mid-send');
|
|
445
|
+
select is((select status || ':' || skip_reason from public.notification_deliveries where notification_id = pg_temp.k('co1')), 'cancelled:collapsed', '... ends collapsed, not back in the queue');
|
|
446
|
+
|
|
447
|
+
-- 0005's switch left on never opts anyone into email; clearing a choice never made is fine.
|
|
448
|
+
update public.notification_types set retired_at = null where key = 'gntest.news';
|
|
449
|
+
update public.notification_preferences set categories = '{"gntest-news": true}', channels = '{}' where user_id = pg_temp.u('c3');
|
|
450
|
+
select pg_temp.sign_in('c3');
|
|
451
|
+
select is(public.genesis_notifications_set_preference('gntest-news', 'in_app', false), '{"gntest-news": {"in_app": false}}'::jsonb,
|
|
452
|
+
'an old switch left on carries over as nothing, not as email on');
|
|
453
|
+
select lives_ok($$ select public.genesis_notifications_set_preference('gntest-forms', 'email', null) $$, 'clearing a choice in a category with none is fine');
|
|
454
|
+
reset role;
|
|
455
|
+
select is(private.genesis_notifications_channel_on(pg_temp.u('c3'), null, 'gntest.news', 'email'), false, '... so opt-in email stays off');
|
|
456
|
+
|
|
457
|
+
-- Secrets: relative links and any parameter ending like one.
|
|
458
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000b2"]', '{"link": "/accept?inviteCode=abc"}', 'rel-1') $$,
|
|
459
|
+
'22023', null, 'a relative link carrying a code is refused');
|
|
460
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000b2"]', '{"next": "?otp=123456"}', 'rel-2') $$,
|
|
461
|
+
'22023', null, '... and a bare query');
|
|
462
|
+
select throws_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000b2"]', '{"verification_code": "1"}', 'rel-3') $$,
|
|
463
|
+
'22023', null, '... and a key ending in code');
|
|
464
|
+
select lives_ok($$ select * from public.genesis_notify('gntest.message', '["00000000-0000-4000-8000-0000000000b2"]', '{"link": "/messages/42?tab=files"}', 'rel-4') $$,
|
|
465
|
+
'... but not a plain link');
|
|
466
|
+
|
|
467
|
+
-- Erasing a login erases mail addressed to its email.
|
|
468
|
+
insert into k select 'em', notification_id from public.genesis_notify('gntest.receipt', '[{"email": "GNTEST-b2@example.test"}]', '{}', 'erase-mail');
|
|
469
|
+
select ok(public.genesis_notifications_erase(pg_temp.u('b2')) >= 1, 'erasing a login');
|
|
470
|
+
select is((select count(*) from public.notifications where id = pg_temp.k('em')), 0::bigint, '... also erases what was addressed to its email');
|
|
471
|
+
|
|
472
|
+
select * from finish();
|
|
473
|
+
rollback;
|