fix(sandbox): use posting engine and recover failed seeds (#2297)
* fix(sandbox): seed through posting engine and recover failed attempts * test(sandbox): align CI auth schema for anonymous users
This commit is contained in:
@@ -47,6 +47,12 @@ AS $$
|
||||
SELECT string_to_array(name, '/');
|
||||
$$;
|
||||
|
||||
-- auth.identities is created by GoTrue at startup, not by the Postgres
|
||||
-- image. Likewise, GoTrue's 20240214120130 migration adds is_anonymous;
|
||||
-- sandbox authorization reads this server-owned flag, not user metadata.
|
||||
ALTER TABLE auth.users
|
||||
ADD COLUMN IF NOT EXISTS is_anonymous boolean NOT NULL DEFAULT false;
|
||||
|
||||
-- auth.identities is created by GoTrue at startup, not by the Postgres
|
||||
-- image, and GoTrue does not run in CI. Triggers on auth.users that touch
|
||||
-- identities (20260903110000 unlink_old_address_identities) need the table
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, it } from 'vitest'
|
||||
import { getPool } from './setup'
|
||||
|
||||
describe('sandbox seed attempt lifecycle', () => {
|
||||
it('serializes claims, isolates users, preserves partial data and fences stale completions', async () => {
|
||||
const client = await getPool().connect()
|
||||
try {
|
||||
await client.query(readFileSync(resolve('tests/pg/sandbox-seed-attempts.sql'), 'utf8'))
|
||||
} finally {
|
||||
await client.query('ROLLBACK')
|
||||
client.release()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
-- Shared by pg-real and the staging MCP verification. All fixtures roll back.
|
||||
BEGIN;
|
||||
DO $$
|
||||
DECLARE
|
||||
u uuid := gen_random_uuid();
|
||||
other_user uuid := gen_random_uuid();
|
||||
a jsonb;
|
||||
b jsonb;
|
||||
old_company uuid;
|
||||
attempt uuid;
|
||||
BEGIN
|
||||
INSERT INTO auth.users(id, email, instance_id, is_anonymous)
|
||||
VALUES (u, u::text || '@test.invalid', '00000000-0000-0000-0000-000000000000', true),
|
||||
(other_user, other_user::text || '@test.invalid', '00000000-0000-0000-0000-000000000000', false);
|
||||
PERFORM set_config('request.jwt.claims', jsonb_build_object('sub', u, 'role', 'authenticated', 'is_anonymous', true)::text, true);
|
||||
PERFORM set_config('request.jwt.claim.sub', u::text, true);
|
||||
SET LOCAL ROLE authenticated;
|
||||
a := public.claim_sandbox_seed();
|
||||
ASSERT a->>'status' = 'running', 'first request must claim';
|
||||
old_company := (a->>'company_id')::uuid;
|
||||
attempt := (a->>'attempt_id')::uuid;
|
||||
ASSERT (SELECT active_company_id = old_company FROM public.user_preferences WHERE user_id = u);
|
||||
ASSERT (SELECT is_sandbox FROM public.company_settings WHERE company_id = old_company);
|
||||
ASSERT (public.claim_sandbox_seed()->>'status') = 'busy', 'second request must not seed';
|
||||
ASSERT NOT public.finish_sandbox_seed(gen_random_uuid(), true), 'wrong token must not finish';
|
||||
ASSERT public.finish_sandbox_seed(attempt, false), 'failed attempt must release';
|
||||
b := public.claim_sandbox_seed();
|
||||
ASSERT b->>'status' = 'running';
|
||||
ASSERT b->>'company_id' <> a->>'company_id', 'retry must use a fresh company';
|
||||
-- Active-company RLS intentionally hides the previous archived company.
|
||||
RESET ROLE;
|
||||
ASSERT EXISTS (SELECT 1 FROM public.companies WHERE id = old_company AND archived_at IS NOT NULL);
|
||||
ASSERT EXISTS (SELECT 1 FROM public.company_settings WHERE company_id = old_company), 'preserve partial data';
|
||||
SET LOCAL ROLE authenticated;
|
||||
ASSERT NOT public.finish_sandbox_seed(attempt, true), 'old worker cannot finish new attempt';
|
||||
ASSERT public.finish_sandbox_seed((b->>'attempt_id')::uuid, true);
|
||||
ASSERT NOT public.finish_sandbox_seed((b->>'attempt_id')::uuid, false), 'late error cannot downgrade success';
|
||||
ASSERT public.claim_sandbox_seed()->>'status' = 'complete';
|
||||
ASSERT public.claim_sandbox_seed()->>'company_id' = b->>'company_id';
|
||||
BEGIN
|
||||
UPDATE public.sandbox_seed_attempts SET status = 'complete' WHERE user_id = u;
|
||||
RAISE EXCEPTION 'direct state writes must be denied';
|
||||
EXCEPTION WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
RESET ROLE;
|
||||
PERFORM set_config('request.jwt.claim.sub', other_user::text, true);
|
||||
PERFORM set_config('request.jwt.claims', jsonb_build_object('sub', other_user, 'role', 'authenticated')::text, true);
|
||||
SET LOCAL ROLE authenticated;
|
||||
ASSERT NOT EXISTS (SELECT 1 FROM public.sandbox_seed_attempts WHERE user_id = u), 'state must be isolated';
|
||||
BEGIN
|
||||
PERFORM public.claim_sandbox_seed();
|
||||
RAISE EXCEPTION 'registered user must be denied';
|
||||
EXCEPTION WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
BEGIN
|
||||
PERFORM public.finish_sandbox_seed((b->>'attempt_id')::uuid, true);
|
||||
RAISE EXCEPTION 'registered user must not finish';
|
||||
EXCEPTION WHEN insufficient_privilege THEN NULL;
|
||||
END;
|
||||
RESET ROLE;
|
||||
-- Expired workers are replaced, retaining their partial data.
|
||||
UPDATE public.sandbox_seed_attempts SET status = 'running', started_at = now() - interval '16 minutes' WHERE user_id = u;
|
||||
PERFORM set_config('request.jwt.claim.sub', u::text, true);
|
||||
PERFORM set_config('request.jwt.claims', jsonb_build_object('sub', u, 'role', 'authenticated', 'is_anonymous', true)::text, true);
|
||||
SET LOCAL ROLE authenticated;
|
||||
a := public.claim_sandbox_seed();
|
||||
ASSERT a->>'status' = 'running';
|
||||
ASSERT a->>'company_id' <> b->>'company_id';
|
||||
RESET ROLE;
|
||||
END $$;
|
||||
ROLLBACK;
|
||||
Reference in New Issue
Block a user