feat: add booked_slots sync trigger and pgTAP RLS/trigger tests
Includes explicit GRANTs for anon/authenticated/service_role on all public tables: the Supabase CLI's current default no longer auto-exposes new tables, so RLS policies from the initial schema were unreachable without them (every request failed at the privilege check before RLS was evaluated).
This commit is contained in:
39
supabase/migrations/20260808000002_booked_slots_trigger.sql
Normal file
39
supabase/migrations/20260808000002_booked_slots_trigger.sql
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
-- Base table grants for the API roles. The Supabase CLI's current default
|
||||||
|
-- (auto_expose_new_tables unset) no longer auto-grants table privileges to
|
||||||
|
-- anon/authenticated/service_role, so without these GRANTs the RLS policies
|
||||||
|
-- from the initial schema migration are unreachable (every request fails at
|
||||||
|
-- the privilege check before RLS is even evaluated). This is the documented
|
||||||
|
-- "explicit grants + RLS" pattern Supabase recommends going forward; the
|
||||||
|
-- actual access is still fully governed by each table's RLS policies.
|
||||||
|
grant select, insert, update, delete on
|
||||||
|
public.products,
|
||||||
|
public.services,
|
||||||
|
public.schedule_template,
|
||||||
|
public.schedule_exceptions,
|
||||||
|
public.bookings,
|
||||||
|
public.booked_slots,
|
||||||
|
public.inquiries
|
||||||
|
to anon, authenticated, service_role;
|
||||||
|
|
||||||
|
-- Keeps the public, PII-free booked_slots table in sync whenever a booking
|
||||||
|
-- is created. security definer + explicit search_path per Supabase's
|
||||||
|
-- documented safe pattern for definer functions.
|
||||||
|
|
||||||
|
create or replace function public.sync_booked_slot()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.booked_slots (date, time)
|
||||||
|
values (new.date, new.time)
|
||||||
|
on conflict do nothing;
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
create trigger booking_created_sync_slot
|
||||||
|
after insert on public.bookings
|
||||||
|
for each row
|
||||||
|
execute function public.sync_booked_slot();
|
||||||
90
supabase/tests/database/rls.test.sql
Normal file
90
supabase/tests/database/rls.test.sql
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
begin;
|
||||||
|
select plan(11);
|
||||||
|
|
||||||
|
-- Seed reference data as service_role (bypasses RLS)
|
||||||
|
set local role service_role;
|
||||||
|
insert into public.services (id, name, duration_minutes, price_from, description, image)
|
||||||
|
values ('knotless', 'Knotless Braids', 240, 150, 'Tension-free braids.', 'styles/knotless.jpg');
|
||||||
|
insert into public.products (id, name, category, price, image, description, in_stock)
|
||||||
|
values ('edge-control', 'Silk Edge Control', 'hair-care', 12, 'products/edge-control.jpg', 'Hold with shine.', true);
|
||||||
|
reset role;
|
||||||
|
|
||||||
|
-- Anyone can read services
|
||||||
|
set local role anon;
|
||||||
|
select isnt_empty(
|
||||||
|
$$ select * from public.services $$,
|
||||||
|
'anon can read services'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anyone can read products
|
||||||
|
select isnt_empty(
|
||||||
|
$$ select * from public.products $$,
|
||||||
|
'anon can read products'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anon cannot write to services
|
||||||
|
select throws_ok(
|
||||||
|
$$ insert into public.services (id, name, duration_minutes, price_from, description, image)
|
||||||
|
values ('hack', 'Hack', 1, 1, 'x', 'x') $$,
|
||||||
|
'42501',
|
||||||
|
null,
|
||||||
|
'anon cannot insert into services'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anyone can create a booking
|
||||||
|
select lives_ok(
|
||||||
|
$$ insert into public.bookings (service_id, date, time, contact_name, contact_phone, contact_email, contact_notes)
|
||||||
|
values ('knotless', '2026-09-15', '10:00 AM', 'Jane Doe', '555-0100', 'jane@example.com', '') $$,
|
||||||
|
'anon can create a booking'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anon cannot read bookings (PII protection)
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.bookings $$,
|
||||||
|
'anon cannot read bookings'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anyone can create an inquiry
|
||||||
|
select lives_ok(
|
||||||
|
$$ insert into public.inquiries (product_id, contact_name, contact_email, contact_notes)
|
||||||
|
values ('edge-control', 'Jane Doe', 'jane@example.com', 'Is this in stock?') $$,
|
||||||
|
'anon can create an inquiry'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anon cannot read inquiries (PII protection)
|
||||||
|
select is_empty(
|
||||||
|
$$ select * from public.inquiries $$,
|
||||||
|
'anon cannot read inquiries'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anyone can read booked_slots (no PII there)
|
||||||
|
select isnt_empty(
|
||||||
|
$$ select * from public.booked_slots $$,
|
||||||
|
'anon can read booked_slots (populated by the trigger from the booking above)'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Anon cannot write booked_slots directly
|
||||||
|
select throws_ok(
|
||||||
|
$$ insert into public.booked_slots (date, time) values ('2026-09-16', '1:00 PM') $$,
|
||||||
|
'42501',
|
||||||
|
null,
|
||||||
|
'anon cannot insert into booked_slots directly'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- The admin (matching UID) can read bookings
|
||||||
|
set local role authenticated;
|
||||||
|
set local request.jwt.claim.sub = '00000000-0000-0000-0000-000000000001';
|
||||||
|
select isnt_empty(
|
||||||
|
$$ select * from public.bookings $$,
|
||||||
|
'admin can read bookings'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- The admin can write to services
|
||||||
|
select lives_ok(
|
||||||
|
$$ insert into public.services (id, name, duration_minutes, price_from, description, image)
|
||||||
|
values ('cornrows', 'Cornrows', 120, 90, 'Classic style.', 'styles/cornrows.jpg') $$,
|
||||||
|
'admin can insert into services'
|
||||||
|
);
|
||||||
|
|
||||||
|
select * from finish();
|
||||||
|
rollback;
|
||||||
Reference in New Issue
Block a user