pgsql-test-jwt-context

Setting up JWT claims and role-based context for RLS testing with pgsql-test.

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-jwt-context" with this command: npx skills add constructive-io/constructive-skills/constructive-io-constructive-skills-pgsql-test-jwt-context

Setting up JWT claims and role-based context for RLS testing with pgsql-test.

Overview

When testing Row-Level Security (RLS) policies, you need to simulate authenticated users with JWT claims. The pgsql-test library provides the setContext() method to configure PostgreSQL session variables that RLS policies can read.

The setContext API

Use setContext() to simulate different user roles and JWT claims:

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': '00000000-0000-0000-0000-000000000001', 'jwt.claims.org_id': 'acme-corp' });

This applies settings using SET LOCAL statements, ensuring they persist only for the current transaction and maintain proper isolation between tests.

How It Works Internally

The setContext() method generates SQL statements:

-- For the 'role' key, uses SET LOCAL ROLE SET LOCAL ROLE "authenticated";

-- For other keys, uses set_config() with transaction-local scope SELECT set_config('jwt.claims.user_id', '00000000-0000-0000-0000-000000000001', true); SELECT set_config('jwt.claims.org_id', 'acme-corp', true);

The third parameter true in set_config() makes the setting transaction-local, which is essential for test isolation.

The auth() Helper Method

For common authentication patterns, use the auth() helper:

// Simple authenticated user db.auth({ role: 'authenticated', userId: '00000000-0000-0000-0000-000000000001' });

// Custom user ID key db.auth({ role: 'authenticated', userId: '123', userIdKey: 'request.jwt.claims.sub' });

Common JWT Claim Patterns

User Authentication

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': userId });

Organization Context

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': userId, 'jwt.claims.org_id': orgId });

Database Context

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': userId, 'jwt.claims.database_id': databaseId });

Additional Claims

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': userId, 'jwt.claims.user_agent': 'Mozilla/5.0...', 'jwt.claims.ip_address': '127.0.0.1' });

Reading Claims in SQL

Your RLS policies can read these claims using current_setting() :

-- In an RLS policy CREATE POLICY user_isolation ON my_table FOR ALL USING (owner_id = current_setting('jwt.claims.user_id', true)::uuid);

You can also create helper functions:

CREATE FUNCTION current_user_id() RETURNS uuid AS $$ SELECT current_setting('jwt.claims.user_id', true)::uuid; $$ LANGUAGE sql STABLE;

Clearing Context

To reset context between scenarios:

db.clearContext();

This nulls all previously set context variables and resets to the default anonymous role.

Testing Different Access Levels

describe('RLS policies', () => { const USER_1 = '00000000-0000-0000-0000-000000000001'; const USER_2 = '00000000-0000-0000-0000-000000000002';

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

it('user can see their own data', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_1 });

const rows = await db.any('SELECT * FROM my_table WHERE owner_id = $1', [USER_1]);
expect(rows.length).toBeGreaterThan(0);

});

it('user cannot see other users data', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_2 });

const rows = await db.any('SELECT * FROM my_table WHERE owner_id = $1', [USER_1]);
expect(rows.length).toBe(0);

});

it('anonymous users have no access', async () => { db.setContext({ role: 'anonymous' });

const rows = await db.any('SELECT * FROM my_table');
expect(rows.length).toBe(0);

}); });

Context Timing

Call setContext() before beforeEach() to apply context at the start of each test:

describe('authenticated role', () => { beforeEach(async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_ID }); await db.beforeEach(); });

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

it('runs as authenticated', async () => { const res = await db.query(SELECT current_setting('role', true) AS role); expect(res.rows[0].role).toBe('authenticated'); }); });

Or set context within individual tests for scenario-specific testing:

it('switches between users', async () => { db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_1 }); const user1Data = await db.any('SELECT * FROM my_table');

db.setContext({ role: 'authenticated', 'jwt.claims.user_id': USER_2 }); const user2Data = await db.any('SELECT * FROM my_table');

expect(user1Data).not.toEqual(user2Data); });

Best Practices

  • Use predefined UUID constants for test user IDs to ensure consistency

  • Set context before beforeEach() for describe-level defaults

  • Use clearContext() when switching between unrelated scenarios

  • Test both positive cases (user can access) and negative cases (user cannot access)

  • Test anonymous/unauthenticated access explicitly

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.

General

planning-blueprinting

No summary provided by upstream source.

Repository SourceNeeds Review
General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pgsql-parser-testing

No summary provided by upstream source.

Repository SourceNeeds Review