pgsql-test-scenario-setup

Structuring complex test scenarios with proper isolation, transaction management, and multi-client patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "pgsql-test-scenario-setup" with this command: npx skills add constructive-io/constructive-skills/constructive-io-constructive-skills-pgsql-test-scenario-setup

Structuring complex test scenarios with proper isolation, transaction management, and multi-client patterns.

Overview

Complex RLS and database tests often require careful setup: creating users, seeding data, and testing access patterns. This skill covers the patterns for structuring these scenarios with proper test isolation.

The Two-Client Pattern

The getConnections() function returns multiple clients with different privilege levels:

import { getConnections, PgTestClient } from 'pgsql-test';

let db: PgTestClient; // App-level client (RLS-enforced) let pg: PgTestClient; // Superuser client (bypasses RLS) let teardown: () => Promise<void>;

beforeAll(async () => { ({ db, pg, teardown } = await getConnections()); });

afterAll(() => teardown());

When to Use Each Client

Use pg (superuser) for:

  • Test setup that needs to bypass RLS

  • Creating test users and seed data

  • Administrative operations

  • Verifying data exists regardless of RLS

Use db (app-level) for:

  • Testing actual RLS behavior

  • Simulating real application queries

  • Verifying access control works correctly

Example: RLS Test Setup

const TEST_USER_1 = '00000000-0000-0000-0000-000000000001'; const TEST_USER_2 = '00000000-0000-0000-0000-000000000002';

beforeAll(async () => { ({ db, pg, teardown } = await getConnections());

// Use pg (superuser) to create test users - bypasses RLS await pg.query( INSERT INTO users (id, username) VALUES ($1, 'user1') ON CONFLICT DO NOTHING, [TEST_USER_1] ); await pg.query( INSERT INTO users (id, username) VALUES ($1, 'user2') ON CONFLICT DO NOTHING, [TEST_USER_2] );

// Create test data owned by user 1 await pg.query( INSERT INTO documents (owner_id, title) VALUES ($1, 'User 1 Doc'), [TEST_USER_1] ); });

// Now test RLS with db client it('user 1 can see their documents', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': TEST_USER_1 }); const docs = await db.any('SELECT * FROM documents'); expect(docs.length).toBe(1); });

it('user 2 cannot see user 1 documents', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': TEST_USER_2 }); const docs = await db.any('SELECT * FROM documents'); expect(docs.length).toBe(0); });

Transaction Management

Test Isolation with beforeEach/afterEach

Each test runs in its own transaction that rolls back after the test:

beforeEach(async () => { await db.beforeEach(); // BEGIN + SAVEPOINT });

afterEach(async () => { await db.afterEach(); // ROLLBACK TO SAVEPOINT + COMMIT });

This ensures tests don't affect each other - any data created during a test is rolled back.

What beforeEach/afterEach Do

// db.beforeEach() executes: await this.begin(); // BEGIN transaction await this.savepoint(); // SAVEPOINT "lqlsavepoint"

// db.afterEach() executes: await this.rollback(); // ROLLBACK TO SAVEPOINT "lqlsavepoint" await this.commit(); // COMMIT (the outer transaction)

The publish() Method

When you need data created in one client to be visible to another client (or to persist beyond the current transaction), use publish() :

it('cross-connection visibility', async () => { // Create data with db client db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_ID }); await db.query(INSERT INTO items (name) VALUES ('test item'));

// Data is not yet visible to pg client (different connection) let pgItems = await pg.any('SELECT * FROM items WHERE name = $1', ['test item']); expect(pgItems.length).toBe(0);

// Publish makes data visible to other connections await db.publish();

// Now pg can see it pgItems = await pg.any('SELECT * FROM items WHERE name = $1', ['test item']); expect(pgItems.length).toBe(1); });

What publish() Does

// db.publish() executes: await this.commit(); // Make data visible to other sessions await this.begin(); // Start fresh transaction await this.savepoint(); // Maintain rollback harness await this.ctxQuery(); // Reapply setContext() settings

Setup Patterns

Pattern 1: Simple Setup in beforeAll

For tests that share the same seed data:

beforeAll(async () => { ({ db, pg, teardown } = await getConnections());

// Seed data once await pg.query(INSERT INTO users (id, name) VALUES ($1, 'Alice'), [USER_ID]); });

afterAll(() => teardown());

beforeEach(() => db.beforeEach()); afterEach(() => db.afterEach());

Pattern 2: Complex Setup with Transactions

For setup that requires multiple steps with intermediate commits:

beforeAll(async () => { ({ db, pg, teardown } = await getConnections());

// Start transaction on pg for setup await pg.begin(); await pg.savepoint();

// Create users await pg.query(INSERT INTO users (id, name) VALUES ($1, 'Alice'), [USER_1]); await pg.query(INSERT INTO users (id, name) VALUES ($1, 'Bob'), [USER_2]);

// Commit pg's work so db can see it await pg.commit(); await pg.begin(); await pg.savepoint();

// Now db can work with the users await db.begin(); await db.savepoint();

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_1 }); // ... additional setup with RLS context

await db.commit(); await db.begin(); await db.savepoint(); });

Pattern 3: Per-Describe Setup

For describe blocks that need their own isolated setup:

describe('Admin scenarios', () => { let adminId: string;

beforeAll(async () => { // Create admin user for this describe block const result = await pg.one<{ id: string }>( INSERT INTO users (name, is_admin) VALUES ('Admin', true) RETURNING id ); adminId = result.id; });

beforeEach(() => db.beforeEach()); afterEach(() => db.afterEach());

it('admin can see all data', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': adminId }); // ... }); });

Scenario Testing Pattern

For testing complex workflows with multiple actors:

describe('Organization membership scenarios', () => { const OWNER_ID = '00000000-0000-0000-0000-000000000001'; const MEMBER_ID = '00000000-0000-0000-0000-000000000002'; let orgId: string;

beforeAll(async () => { ({ db, pg, teardown } = await getConnections());

// Create test users
await pg.query(`INSERT INTO users (id, name) VALUES ($1, 'Owner')`, [OWNER_ID]);
await pg.query(`INSERT INTO users (id, name) VALUES ($1, 'Member')`, [MEMBER_ID]);

});

afterAll(() => teardown()); beforeEach(() => db.beforeEach()); afterEach(() => db.afterEach());

it('owner creates organization', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': OWNER_ID });

const result = await db.one&#x3C;{ id: string }>(
  `INSERT INTO organizations (name) VALUES ('Acme') RETURNING id`
);
orgId = result.id;

expect(orgId).toBeDefined();

});

it('owner can add members', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': OWNER_ID });

// First recreate the org (previous test rolled back)
const org = await db.one&#x3C;{ id: string }>(
  `INSERT INTO organizations (name) VALUES ('Acme') RETURNING id`
);

await db.query(
  `INSERT INTO memberships (org_id, user_id) VALUES ($1, $2)`,
  [org.id, MEMBER_ID]
);

const members = await db.any(
  `SELECT * FROM memberships WHERE org_id = $1`,
  [org.id]
);
expect(members.length).toBe(1);

}); });

Best Practices

  • Use pg for setup, db for testing RLS behavior

  • Always call beforeEach() /afterEach() for test isolation

  • Use publish() when data needs to be visible across connections

  • Keep test user IDs as constants for consistency

  • Structure complex scenarios with clear beforeAll setup

  • Remember that each test's changes are rolled back - don't depend on previous test state

  • Use descriptive test names that explain the scenario being tested

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

constructive-graphql-codegen

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-workflows-ollama

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-workflows-pgpm

No summary provided by upstream source.

Repository SourceNeeds Review