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:
belviskhoremk
2026-08-08 13:01:08 +00:00
parent 6c1a8a92c5
commit 5e3a77fc70
2 changed files with 129 additions and 0 deletions

View 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();

View 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;