Skip to content

Commit b0a131b

Browse files
claudeHerklos
authored andcommitted
test(supabase): add pgTAP coverage for the money-core and secondary features
Audits the SQL schema feature-by-feature and adds pgTAP tests for the areas that previously had zero coverage — chiefly the revenue/invoicing "money-core" the README claims is verified but that no test exercised. New database tests (73 assertions, all green against a live Postgres): - 080 revenue recognition — recognize_project_revenue across recurring / time_based / manual / self_billing (markup), approved+billable input filter, manager authorization, idempotent auto upsert, retire-on-zero and in-window filtering. - 090 settlement pool — settle_project_month: the README worked example end to end, the paid-revenue funding gate, FIFO by submission_seq with a partial payment, per-freelancer carry-forward across months, fixed-cost deduction, referral first-claim, and authorization. - 100 invoice integrity + referrals — server-side earned/tjm/hpd recompute, tenant integrity, settled-status transitions, cancel-frees-entries, the invoiced-entry freeze, and the <=100% referral guard. - 110 project costs — kind-discriminated CHECK constraints, project_cost_cumulative recurring math, mark_project_costs_paid auth and the reimbursable-not-payable guard. - 120 workflow / blog / notifications — sequential invoice numbering, time-entry approver stamping + submit/review notifications, the correction net-non-negative guard, and blog published-only RLS. Every audited behavior matched the intended design; no schema defects were found. The tests lock that behavior in. Also adds a Docker-free local runner (tests/_run_local.sh + _local_bootstrap.psql) that rebuilds a throwaway DB from a minimal Supabase stand-in + the migrations and runs the suite with pg_prove. Both files are underscore-prefixed and non-.test.sql, so the Supabase test runner skips them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2RkSqhagEjBj8ncHGBGRo
1 parent 1d4100c commit b0a131b

8 files changed

Lines changed: 1048 additions & 0 deletions

backend/supabase/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,32 @@ pnpm types # regenerate ../../packages/sdk/src/schema.ts
3737
pnpm push # db push to the linked project
3838
pnpm test # pgTAP database tests (invites + rem compute/security)
3939
```
40+
41+
## Database tests (pgTAP)
42+
43+
`supabase/tests/database/*.test.sql` — run with `pnpm test` (needs the Supabase
44+
CLI + Docker). Coverage:
45+
46+
| File | Feature area |
47+
|---|---|
48+
| `000` | shared harness + helpers |
49+
| `010``040` | company invites — accept lifecycle, escalation guards, RLS, seat/edge |
50+
| `050``070` | unified remuneration ("rem") — compute golden math, security/capacity, jungle FIFO + staffing fee |
51+
| `080` | revenue recognition — `recognize_project_revenue` (recurring / time_based / manual / self_billing markup, auth, retire-on-zero, in-window) |
52+
| `090` | settlement — `settle_project_month` funding pool: README worked example, paid-revenue gate, FIFO by `submission_seq`, per-freelancer carry-forward, fixed-cost deduction, referral first-claim, auth |
53+
| `100` | invoice integrity + referrals — server recompute, tenant integrity, settled-status transitions, cancel-frees-entries, invoiced-entry freeze, `enforce_referral_total` ≤100% |
54+
| `110` | project costs — kind-discriminated CHECKs, `project_cost_cumulative`, `mark_project_costs_paid` auth + reimbursable guard |
55+
| `120` | workflow/blog/notifications — sequential invoice numbering, time-entry approver stamping + submit/review notifications, correction net-non-negative guard, blog published-only RLS |
56+
57+
### Running the tests without Docker
58+
59+
`tests/_run_local.sh` rebuilds a throwaway DB from `tests/_local_bootstrap.psql`
60+
(a minimal local stand-in for the Supabase platform: roles, `auth`/`storage`
61+
schemas, `auth.uid()`) + every migration, then runs the suite with `pg_prove`.
62+
Needs a running PostgreSQL and `postgresql-<v>-pgtap`. Neither `_`-prefixed file
63+
is a test (they are skipped by the `*.test.sql` runner):
64+
65+
```bash
66+
# against a local PG on port 54399, as the postgres superuser
67+
PGPORT=54399 bash tests/_run_local.sh
68+
```
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
-- Local-only Supabase scaffolding so the migrations + pgTAP suite can run against
2+
-- a plain PostgreSQL cluster (no supabase CLI / Docker). NOT a migration — this
3+
-- file is never shipped or applied to the hosted DB; it recreates just enough of
4+
-- the platform (roles, auth + storage schemas, auth.uid()) for the tests.
5+
6+
-- ── Roles ────────────────────────────────────────────────────────────────────
7+
do $$
8+
begin
9+
if not exists (select from pg_roles where rolname = 'anon') then create role anon nologin noinherit; end if;
10+
if not exists (select from pg_roles where rolname = 'authenticated') then create role authenticated nologin noinherit; end if;
11+
if not exists (select from pg_roles where rolname = 'service_role') then create role service_role nologin noinherit bypassrls; end if;
12+
if not exists (select from pg_roles where rolname = 'authenticator') then create role authenticator noinherit login; end if;
13+
if not exists (select from pg_roles where rolname = 'supabase_auth_admin') then create role supabase_auth_admin noinherit; end if;
14+
if not exists (select from pg_roles where rolname = 'supabase_storage_admin') then create role supabase_storage_admin noinherit; end if;
15+
end $$;
16+
17+
grant anon, authenticated, service_role to authenticator;
18+
grant anon, authenticated, service_role to postgres;
19+
20+
-- ── Schemas ──────────────────────────────────────────────────────────────────
21+
create schema if not exists extensions;
22+
create schema if not exists auth authorization supabase_auth_admin;
23+
create schema if not exists storage authorization supabase_storage_admin;
24+
25+
grant usage on schema extensions to anon, authenticated, service_role;
26+
grant usage on schema auth to anon, authenticated, service_role, postgres;
27+
grant usage on schema storage to anon, authenticated, service_role, postgres;
28+
29+
-- ── Extensions ───────────────────────────────────────────────────────────────
30+
create extension if not exists pgcrypto with schema extensions;
31+
create extension if not exists "uuid-ossp" with schema extensions;
32+
grant execute on all functions in schema extensions to anon, authenticated, service_role, postgres;
33+
34+
-- ── auth.users / auth.identities (minimal shape used by tests + FKs) ──────────
35+
create table if not exists auth.users (
36+
instance_id uuid,
37+
id uuid primary key,
38+
aud varchar(255),
39+
role varchar(255),
40+
email varchar(255),
41+
encrypted_password varchar(255),
42+
email_confirmed_at timestamptz,
43+
invited_at timestamptz,
44+
raw_app_meta_data jsonb,
45+
raw_user_meta_data jsonb,
46+
is_super_admin boolean,
47+
created_at timestamptz,
48+
updated_at timestamptz,
49+
phone text default null,
50+
deleted_at timestamptz
51+
);
52+
53+
create table if not exists auth.identities (
54+
id uuid primary key default gen_random_uuid(),
55+
provider_id text,
56+
user_id uuid not null references auth.users (id) on delete cascade,
57+
identity_data jsonb not null,
58+
provider text not null,
59+
last_sign_in_at timestamptz,
60+
created_at timestamptz,
61+
updated_at timestamptz
62+
);
63+
64+
grant all on auth.users to postgres, supabase_auth_admin;
65+
grant all on auth.identities to postgres, supabase_auth_admin;
66+
67+
-- ── auth.uid() / auth.role() / auth.jwt() ─────────────────────────────────────
68+
create or replace function auth.uid() returns uuid
69+
language sql stable
70+
as $$
71+
select coalesce(
72+
nullif(current_setting('request.jwt.claim.sub', true), ''),
73+
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
74+
)::uuid
75+
$$;
76+
77+
create or replace function auth.role() returns text
78+
language sql stable
79+
as $$
80+
select coalesce(
81+
nullif(current_setting('request.jwt.claim.role', true), ''),
82+
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role')
83+
)::text
84+
$$;
85+
86+
create or replace function auth.jwt() returns jsonb
87+
language sql stable
88+
as $$
89+
select coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb, '{}'::jsonb)
90+
$$;
91+
92+
grant execute on function auth.uid(), auth.role(), auth.jwt() to anon, authenticated, service_role, postgres;
93+
94+
-- ── storage.buckets / storage.objects + helpers ───────────────────────────────
95+
create table if not exists storage.buckets (
96+
id text primary key,
97+
name text not null,
98+
owner uuid,
99+
public boolean default false,
100+
avif_autodetection boolean default false,
101+
file_size_limit bigint,
102+
allowed_mime_types text[],
103+
created_at timestamptz default now(),
104+
updated_at timestamptz default now()
105+
);
106+
107+
create table if not exists storage.objects (
108+
id uuid primary key default gen_random_uuid(),
109+
bucket_id text references storage.buckets (id),
110+
name text,
111+
owner uuid,
112+
metadata jsonb,
113+
created_at timestamptz default now(),
114+
updated_at timestamptz default now(),
115+
last_accessed_at timestamptz default now(),
116+
path_tokens text[]
117+
);
118+
alter table storage.objects enable row level security;
119+
120+
create or replace function storage.foldername(name text) returns text[]
121+
language plpgsql stable
122+
as $$
123+
declare _parts text[];
124+
begin
125+
select string_to_array(name, '/') into _parts;
126+
return _parts[1 : array_length(_parts, 1) - 1];
127+
end
128+
$$;
129+
130+
create or replace function storage.filename(name text) returns text
131+
language plpgsql stable
132+
as $$
133+
declare _parts text[];
134+
begin
135+
select string_to_array(name, '/') into _parts;
136+
return _parts[array_length(_parts, 1)];
137+
end
138+
$$;
139+
140+
grant all on storage.buckets, storage.objects to postgres, supabase_storage_admin, service_role;
141+
grant select on storage.buckets, storage.objects to anon, authenticated;
142+
grant insert, update, delete on storage.objects to authenticated;
143+
grant execute on function storage.foldername(text), storage.filename(text) to anon, authenticated, service_role, postgres;
144+
145+
-- Default privileges so objects created by later migrations are reachable by the
146+
-- API roles the way Supabase configures them.
147+
alter default privileges in schema public grant all on tables to anon, authenticated, service_role;
148+
alter default privileges in schema public grant all on functions to anon, authenticated, service_role;
149+
alter default privileges in schema public grant all on sequences to anon, authenticated, service_role;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
# Local pgTAP runner: rebuild a throwaway DB from bootstrap + migrations (+seed),
3+
# then run the pgTAP suite with pg_prove. Requires a running PG on $PGPORT.
4+
set -euo pipefail
5+
6+
PGPORT="${PGPORT:-54399}"
7+
PGHOST="${PGHOST:-127.0.0.1}"
8+
DBNAME="${DBNAME:-chrono_test}"
9+
export PGPASSWORD=""
10+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
SUPA="$(cd "$HERE/.." && pwd)"
12+
PSQL="psql -v ON_ERROR_STOP=1 -h $PGHOST -p $PGPORT -U postgres -q"
13+
14+
SEED="${SEED:-0}"
15+
16+
echo "== drop/create $DBNAME =="
17+
psql -h "$PGHOST" -p "$PGPORT" -U postgres -q -c "drop database if exists $DBNAME;" -c "create database $DBNAME;"
18+
# pgTAP + Chrono's extensions live in the extensions schema (as on Supabase); put it
19+
# on the search_path so unqualified plan()/ok()/gen_random_uuid() resolve.
20+
psql -h "$PGHOST" -p "$PGPORT" -U postgres -q -c "alter database $DBNAME set search_path = public, extensions;"
21+
22+
echo "== bootstrap =="
23+
$PSQL -d "$DBNAME" -f "$HERE/_local_bootstrap.psql" >/dev/null
24+
25+
echo "== migrations =="
26+
for f in $(ls "$SUPA"/migrations/*.sql | sort); do
27+
$PSQL -d "$DBNAME" -f "$f" >/dev/null
28+
done
29+
30+
if [ "$SEED" = "1" ]; then
31+
echo "== seed =="
32+
$PSQL -d "$DBNAME" -f "$SUPA/seed.sql" >/dev/null
33+
fi
34+
35+
echo "== pgTAP tests =="
36+
pg_prove -h "$PGHOST" -p "$PGPORT" -U postgres -d "$DBNAME" --ext .sql "$@" "$HERE"/database/*.test.sql

0 commit comments

Comments
 (0)