-
Notifications
You must be signed in to change notification settings - Fork 12
/
supabase_test_helpers--0.0.6.sql
385 lines (351 loc) · 12.5 KB
/
supabase_test_helpers--0.0.6.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION supabase_test_helpers" to load this file. \quit
-- We want to store all of this in the tests schema to keep it
-- separate from any application data
CREATE SCHEMA IF NOT EXISTS tests;
--- Create a specific schema for override functions so we don't have to worry about
--- anything else be adding to the tests schema
CREATE SCHEMA IF NOT EXISTS test_overrides;
-- anon, authenticated, and service_role should have access to tests schema
GRANT USAGE ON SCHEMA tests TO anon, authenticated, service_role;
-- Don't allow public to execute any functions in the tests schema
ALTER DEFAULT PRIVILEGES IN SCHEMA tests REVOKE EXECUTE ON FUNCTIONS FROM public;
-- Grant execute to anon, authenticated, and service_role for testing purposes
ALTER DEFAULT PRIVILEGES IN SCHEMA tests GRANT EXECUTE ON FUNCTIONS TO anon, authenticated, service_role;
-- anon, authenticated, and service_role should have access to test_overrides schema
GRANT USAGE ON SCHEMA test_overrides TO anon, authenticated, service_role;
-- Don't allow public to execute any functions in the test_overrides schema
ALTER DEFAULT PRIVILEGES IN SCHEMA test_overrides REVOKE EXECUTE ON FUNCTIONS FROM public;
-- Grant execute to anon, authenticated, and service_role for testing purposes
ALTER DEFAULT PRIVILEGES IN SCHEMA test_overrides GRANT EXECUTE ON FUNCTIONS TO anon, authenticated, service_role;
/**
* ### tests.create_supabase_user(identifier text, email text, phone text)
*
* Creates a new user in the `auth.users` table.
* You can recall a user's info by using `tests.get_supabase_user(identifier text)`.
*
* Parameters:
* - `identifier` - A unique identifier for the user. We recommend you keep it memorable like "test_owner" or "test_member"
* - `email` - (Optional) The email address of the user
* - `phone` - (Optional) The phone number of the user
* - `metadata` - (Optional) Additional metadata to be added to the user
*
* Returns:
* - `user_id` - The UUID of the user in the `auth.users` table
*
* Example:
* ```sql
* SELECT tests.create_supabase_user('test_owner');
* SELECT tests.create_supabase_user('test_member', '[email protected]', '555-555-5555');
* SELECT tests.create_supabase_user('test_member', '[email protected]', '555-555-5555', '{"key": "value"}'::jsonb);
* ```
*/
CREATE OR REPLACE FUNCTION tests.create_supabase_user(identifier text, email text default null, phone text default null, metadata jsonb default null)
RETURNS uuid
SECURITY DEFINER
SET search_path = auth, pg_temp
AS $$
DECLARE
user_id uuid;
BEGIN
-- create the user
user_id := extensions.uuid_generate_v4();
INSERT INTO auth.users (id, email, phone, raw_user_meta_data, raw_app_meta_data, created_at, updated_at)
VALUES (user_id, coalesce(email, concat(user_id, '@test.com')), phone, jsonb_build_object('test_identifier', identifier) || coalesce(metadata, '{}'::jsonb), '{}'::jsonb, now(), now())
RETURNING id INTO user_id;
RETURN user_id;
END;
$$ LANGUAGE plpgsql;
/**
* ### tests.get_supabase_user(identifier text)
*
* Returns the user info for a user created with `tests.create_supabase_user`.
*
* Parameters:
* - `identifier` - The unique identifier for the user
*
* Returns:
* - `user_id` - The UUID of the user in the `auth.users` table
*
* Example:
* ```sql
* SELECT posts where posts.user_id = tests.get_supabase_user('test_owner') -> 'id';
* ```
*/
CREATE OR REPLACE FUNCTION tests.get_supabase_user(identifier text)
RETURNS json
SECURITY DEFINER
SET search_path = auth, pg_temp
AS $$
DECLARE
supabase_user json;
BEGIN
SELECT json_build_object(
'id', id,
'email', email,
'phone', phone,
'raw_user_meta_data', raw_user_meta_data,
'raw_app_meta_data', raw_app_meta_data
) into supabase_user
FROM auth.users
WHERE raw_user_meta_data ->> 'test_identifier' = identifier limit 1;
if supabase_user is null OR supabase_user -> 'id' IS NULL then
RAISE EXCEPTION 'User with identifier % not found', identifier;
end if;
RETURN supabase_user;
END;
$$ LANGUAGE plpgsql;
/**
* ### tests.get_supabase_uid(identifier text)
*
* Returns the user UUID for a user created with `tests.create_supabase_user`.
*
* Parameters:
* - `identifier` - The unique identifier for the user
*
* Returns:
* - `user_id` - The UUID of the user in the `auth.users` table
*
* Example:
* ```sql
* SELECT posts where posts.user_id = tests.get_supabase_uid('test_owner') -> 'id';
* ```
*/
CREATE OR REPLACE FUNCTION tests.get_supabase_uid(identifier text)
RETURNS uuid
SECURITY DEFINER
SET search_path = auth, pg_temp
AS $$
DECLARE
supabase_user uuid;
BEGIN
SELECT id into supabase_user FROM auth.users WHERE raw_user_meta_data ->> 'test_identifier' = identifier limit 1;
if supabase_user is null then
RAISE EXCEPTION 'User with identifier % not found', identifier;
end if;
RETURN supabase_user;
END;
$$ LANGUAGE plpgsql;
/**
* ### tests.authenticate_as(identifier text)
* Authenticates as a user created with `tests.create_supabase_user`.
*
* Parameters:
* - `identifier` - The unique identifier for the user
*
* Returns:
* - `void`
*
* Example:
* ```sql
* SELECT tests.create_supabase_user('test_owner');
* SELECT tests.authenticate_as('test_owner');
* ```
*/
CREATE OR REPLACE FUNCTION tests.authenticate_as (identifier text)
RETURNS void
AS $$
DECLARE
user_data json;
original_auth_data text;
BEGIN
-- store the request.jwt.claims in a variable in case we need it
original_auth_data := current_setting('request.jwt.claims', true);
user_data := tests.get_supabase_user(identifier);
if user_data is null OR user_data ->> 'id' IS NULL then
RAISE EXCEPTION 'User with identifier % not found', identifier;
end if;
perform set_config('role', 'authenticated', true);
perform set_config('request.jwt.claims', json_build_object(
'sub', user_data ->> 'id',
'email', user_data ->> 'email',
'phone', user_data ->> 'phone',
'user_metadata', user_data -> 'raw_user_meta_data',
'app_metadata', user_data -> 'raw_app_meta_data'
)::text, true);
EXCEPTION
-- revert back to original auth data
WHEN OTHERS THEN
set local role authenticated;
set local "request.jwt.claims" to original_auth_data;
RAISE;
END
$$ LANGUAGE plpgsql;
/**
* ### tests.authenticate_as_service_role()
* Clears authentication object and sets role to service_role.
*
* Returns:
* - `void`
*
* Example:
* ```sql
* SELECT tests.authenticate_as_service_role();
* ```
*/
CREATE OR REPLACE FUNCTION tests.authenticate_as_service_role ()
RETURNS void
AS $$
BEGIN
perform set_config('role', 'service_role', true);
perform set_config('request.jwt.claims', null, true);
END
$$ LANGUAGE plpgsql;
/**
* ### tests.clear_authentication()
* Clears out the authentication and sets role to anon
*
* Returns:
* - `void`
*
* Example:
* ```sql
* SELECT tests.create_supabase_user('test_owner');
* SELECT tests.authenticate_as('test_owner');
* SELECT tests.clear_authentication();
* ```
*/
CREATE OR REPLACE FUNCTION tests.clear_authentication()
RETURNS void AS $$
BEGIN
perform set_config('role', 'anon', true);
perform set_config('request.jwt.claims', null, true);
END
$$ LANGUAGE plpgsql;
/**
* ### tests.rls_enabled(testing_schema text)
* pgTAP function to check if RLS is enabled on all tables in a provided schema
*
* Parameters:
* - schema_name text - The name of the schema to check
*
* Example:
* ```sql
* BEGIN;
* select plan(1);
* select tests.rls_enabled('public');
* SELECT * FROM finish();
* ROLLBACK;
* ```
*/
CREATE OR REPLACE FUNCTION tests.rls_enabled (testing_schema text)
RETURNS text AS $$
select is(
(select
count(pc.relname)::integer
from pg_class pc
join pg_namespace pn on pn.oid = pc.relnamespace and pn.nspname = rls_enabled.testing_schema
join pg_type pt on pt.oid = pc.reltype
where relrowsecurity = FALSE)
,
0,
'All tables in the' || testing_schema || ' schema should have row level security enabled');
$$ LANGUAGE sql;
/**
* ### tests.rls_enabled(testing_schema text, testing_table text)
* pgTAP function to check if RLS is enabled on a specific table
*
* Parameters:
* - schema_name text - The name of the schema to check
* - testing_table text - The name of the table to check
*
* Example:
* ```sql
* BEGIN;
* select plan(1);
* select tests.rls_enabled('public', 'accounts');
* SELECT * FROM finish();
* ROLLBACK;
* ```
*/
CREATE OR REPLACE FUNCTION tests.rls_enabled (testing_schema text, testing_table text)
RETURNS TEXT AS $$
select is(
(select
count(*)::integer
from pg_class pc
join pg_namespace pn on pn.oid = pc.relnamespace and pn.nspname = rls_enabled.testing_schema and pc.relname = rls_enabled.testing_table
join pg_type pt on pt.oid = pc.reltype
where relrowsecurity = TRUE),
1,
testing_table || 'table in the' || testing_schema || ' schema should have row level security enabled'
);
$$ LANGUAGE sql;
--
-- Generated now() function used to replace pg_catalog.now() for the purpose
-- of freezing time in tests. This should not be used directly.
--
CREATE OR REPLACE FUNCTION test_overrides.now()
RETURNS timestamp with time zone
AS $$
BEGIN
-- check if a frozen time is set
IF nullif(current_setting('tests.frozen_time'), '') IS NOT NULL THEN
RETURN current_setting('tests.frozen_time')::timestamptz;
END IF;
RETURN pg_catalog.now();
END
$$ LANGUAGE plpgsql;
/**
* ### tests.freeze_time(frozen_time timestamp with time zone)
*
* Overwrites the current time from now() to the provided time.
*
* Works out of the box for any normal usage of now(), if you have a function that sets its own search path, such as security definers, then you will need to alter the function to set the search path to include test_overrides BEFORE pg_catalog.
* **ONLY do this inside of a pgtap test transaction.**
* Example:
*
* ```sql
* ALTER FUNCTION auth.your_function() SET search_path = test_overrides, public, pg_temp, pg_catalog;
* ```
* View a test example in 05-frozen-time.sql: https://github.com/usebasejump/supabase-test-helpers/blob/main/supabase/tests/05-frozen-time.sql
*
* Parameters:
* - `frozen_time` - The time to freeze to. Supports timestamp with time zone, without time zone, date or any other value that can be coerced into a timestamp with time zone.
*
* Returns:
* - void
*
* Example:
* ```sql
* SELECT tests.freeze_time('2020-01-01 00:00:00');
* ```
*/
CREATE OR REPLACE FUNCTION tests.freeze_time(frozen_time timestamp with time zone)
RETURNS void
AS $$
BEGIN
-- Add test_overrides to search path if needed
IF current_setting('search_path') NOT LIKE 'test_overrides,%' THEN
-- store search path for later
PERFORM set_config('tests.original_search_path', current_setting('search_path'), true);
-- add tests schema to start of search path
PERFORM set_config('search_path', 'test_overrides,' || current_setting('tests.original_search_path') || ',pg_catalog', true);
END IF;
-- create an overwriting now function
PERFORM set_config('tests.frozen_time', frozen_time::text, true);
END
$$ LANGUAGE plpgsql;
/**
* ### tests.unfreeze_time()
*
* Unfreezes the time and restores the original now() function.
*
* Returns:
* - void
*
* Example:
* ```sql
* SELECT tests.unfreeze_time();
* ```
*/
CREATE OR REPLACE FUNCTION tests.unfreeze_time()
RETURNS void
AS $$
BEGIN
-- restore the original now function
PERFORM set_config('tests.frozen_time', null, true);
-- restore the original search path
PERFORM set_config('search_path', current_setting('tests.original_search_path'), true);
END
$$ LANGUAGE plpgsql;