|
| 1 | +process.env.LOG_SCOPE = 'pgsql-test'; |
| 2 | + |
| 3 | +import { getConnections } from '../src/connect'; |
| 4 | +import { getRoleName } from '../src/roles'; |
| 5 | +import { PgTestClient } from '../src/test-client'; |
| 6 | + |
| 7 | +let db: PgTestClient; |
| 8 | +let pg: PgTestClient; |
| 9 | +let teardown: () => Promise<void>; |
| 10 | + |
| 11 | +beforeAll(async () => { |
| 12 | + ({ db, pg, teardown } = await getConnections()); |
| 13 | + |
| 14 | + await pg.query(` |
| 15 | + CREATE TABLE IF NOT EXISTS test_users ( |
| 16 | + id SERIAL PRIMARY KEY, |
| 17 | + email TEXT NOT NULL, |
| 18 | + name TEXT |
| 19 | + ) |
| 20 | + `); |
| 21 | + |
| 22 | + // Grant permissions to authenticated role |
| 23 | + await pg.query(`GRANT ALL ON test_users TO authenticated`); |
| 24 | + await pg.query(`GRANT ALL ON SEQUENCE test_users_id_seq TO authenticated`); |
| 25 | +}); |
| 26 | + |
| 27 | +afterAll(async () => { |
| 28 | + await pg.query(`DROP TABLE IF EXISTS test_users`); |
| 29 | + await teardown(); |
| 30 | +}); |
| 31 | + |
| 32 | +describe('auth() method', () => { |
| 33 | + beforeEach(async () => { |
| 34 | + await db.beforeEach(); |
| 35 | + }); |
| 36 | + |
| 37 | + afterEach(async () => { |
| 38 | + await db.afterEach(); |
| 39 | + }); |
| 40 | + |
| 41 | + it('sets role and userId', async () => { |
| 42 | + const authRole = getRoleName('authenticated'); |
| 43 | + db.auth({ role: authRole, userId: '12345' }); |
| 44 | + |
| 45 | + const role = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 46 | + expect(role.rows[0].role).toBe(authRole); |
| 47 | + |
| 48 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 49 | + expect(userId.rows[0].user_id).toBe('12345'); |
| 50 | + }); |
| 51 | + |
| 52 | + it('sets role with custom userIdKey', async () => { |
| 53 | + const authRole = getRoleName('authenticated'); |
| 54 | + db.auth({ |
| 55 | + role: authRole, |
| 56 | + userId: 'custom-123', |
| 57 | + userIdKey: 'app.user.id' |
| 58 | + }); |
| 59 | + |
| 60 | + const role = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 61 | + expect(role.rows[0].role).toBe(authRole); |
| 62 | + |
| 63 | + const userId = await db.query('SELECT current_setting(\'app.user.id\', true) AS user_id'); |
| 64 | + expect(userId.rows[0].user_id).toBe('custom-123'); |
| 65 | + }); |
| 66 | + |
| 67 | + it('handles numeric userId', async () => { |
| 68 | + const authRole = getRoleName('authenticated'); |
| 69 | + db.auth({ role: authRole, userId: 99999 }); |
| 70 | + |
| 71 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 72 | + expect(userId.rows[0].user_id).toBe('99999'); |
| 73 | + }); |
| 74 | + |
| 75 | + it('sets role without userId', async () => { |
| 76 | + const authRole = getRoleName('authenticated'); |
| 77 | + db.auth({ role: authRole }); |
| 78 | + |
| 79 | + const role = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 80 | + expect(role.rows[0].role).toBe(authRole); |
| 81 | + }); |
| 82 | +}); |
| 83 | + |
| 84 | +describe('auth() with default role', () => { |
| 85 | + beforeEach(async () => { |
| 86 | + await db.beforeEach(); |
| 87 | + }); |
| 88 | + |
| 89 | + afterEach(async () => { |
| 90 | + await db.afterEach(); |
| 91 | + }); |
| 92 | + |
| 93 | + it('uses default authenticated role when role not provided', async () => { |
| 94 | + db.auth({ userId: 'test-user-456' }); |
| 95 | + |
| 96 | + const role = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 97 | + const expectedRole = getRoleName('authenticated'); |
| 98 | + expect(role.rows[0].role).toBe(expectedRole); |
| 99 | + |
| 100 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 101 | + expect(userId.rows[0].user_id).toBe('test-user-456'); |
| 102 | + }); |
| 103 | + |
| 104 | + it('allows explicit role override', async () => { |
| 105 | + const anonRole = getRoleName('anonymous'); |
| 106 | + db.auth({ userId: 'admin-user-789', role: anonRole }); |
| 107 | + |
| 108 | + const role = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 109 | + expect(role.rows[0].role).toBe(anonRole); |
| 110 | + |
| 111 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 112 | + expect(userId.rows[0].user_id).toBe('admin-user-789'); |
| 113 | + }); |
| 114 | + |
| 115 | + it('handles numeric userId with default role', async () => { |
| 116 | + db.auth({ userId: 42 }); |
| 117 | + |
| 118 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 119 | + expect(userId.rows[0].user_id).toBe('42'); |
| 120 | + }); |
| 121 | +}); |
| 122 | + |
| 123 | +describe('clearContext() method', () => { |
| 124 | + beforeEach(async () => { |
| 125 | + await db.beforeEach(); |
| 126 | + }); |
| 127 | + |
| 128 | + afterEach(async () => { |
| 129 | + await db.afterEach(); |
| 130 | + }); |
| 131 | + |
| 132 | + it('clears all session variables', async () => { |
| 133 | + const authRole = getRoleName('authenticated'); |
| 134 | + db.setContext({ |
| 135 | + role: authRole, |
| 136 | + 'jwt.claims.user_id': 'test-123', |
| 137 | + 'jwt.claims.ip_address': '192.168.1.1', |
| 138 | + 'custom.var': 'custom-value' |
| 139 | + }); |
| 140 | + |
| 141 | + const roleBefore = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 142 | + expect(roleBefore.rows[0].role).toBe(authRole); |
| 143 | + |
| 144 | + const userIdBefore = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 145 | + expect(userIdBefore.rows[0].user_id).toBe('test-123'); |
| 146 | + |
| 147 | + db.clearContext(); |
| 148 | + |
| 149 | + const defaultRole = getRoleName('anonymous'); |
| 150 | + const roleAfter = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 151 | + expect(roleAfter.rows[0].role).toBe(defaultRole); |
| 152 | + |
| 153 | + const userIdAfter = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 154 | + expect(userIdAfter.rows[0].user_id).toBe(''); |
| 155 | + |
| 156 | + }); |
| 157 | + |
| 158 | + it('allows setting new context after clearing', async () => { |
| 159 | + const authRole = getRoleName('authenticated'); |
| 160 | + db.setContext({ |
| 161 | + role: authRole, |
| 162 | + 'jwt.claims.user_id': 'old-user' |
| 163 | + }); |
| 164 | + |
| 165 | + const userIdBefore = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 166 | + expect(userIdBefore.rows[0].user_id).toBe('old-user'); |
| 167 | + |
| 168 | + db.clearContext(); |
| 169 | + db.auth({ userId: 'new-user' }); |
| 170 | + |
| 171 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 172 | + expect(userId.rows[0].user_id).toBe('new-user'); |
| 173 | + }); |
| 174 | +}); |
| 175 | + |
| 176 | +describe('publish() method', () => { |
| 177 | + beforeEach(async () => { |
| 178 | + await pg.beforeEach(); |
| 179 | + await db.beforeEach(); |
| 180 | + }); |
| 181 | + |
| 182 | + afterEach(async () => { |
| 183 | + await db.afterEach(); |
| 184 | + await pg.afterEach(); |
| 185 | + }); |
| 186 | + |
| 187 | + it('makes data visible to other connections after publish', async () => { |
| 188 | + await pg.query(`INSERT INTO test_users (email, name) VALUES ('alice@test.com', 'Alice')`); |
| 189 | + |
| 190 | + const beforePublish = await db.any('SELECT * FROM test_users WHERE email = $1', ['alice@test.com']); |
| 191 | + expect(beforePublish.length).toBe(0); |
| 192 | + |
| 193 | + await pg.publish(); |
| 194 | + |
| 195 | + const afterPublish = await db.any('SELECT * FROM test_users WHERE email = $1', ['alice@test.com']); |
| 196 | + expect(afterPublish.length).toBe(1); |
| 197 | + expect(afterPublish[0].email).toBe('alice@test.com'); |
| 198 | + expect(afterPublish[0].name).toBe('Alice'); |
| 199 | + }); |
| 200 | + |
| 201 | + it('preserves context after publish', async () => { |
| 202 | + const authRole = getRoleName('authenticated'); |
| 203 | + db.auth({ role: authRole, userId: 'test-999' }); |
| 204 | + |
| 205 | + await db.publish(); |
| 206 | + |
| 207 | + const role = await db.query('SELECT current_setting(\'role\', true) AS role'); |
| 208 | + expect(role.rows[0].role).toBe(authRole); |
| 209 | + |
| 210 | + const userId = await db.query('SELECT current_setting(\'jwt.claims.user_id\', true) AS user_id'); |
| 211 | + expect(userId.rows[0].user_id).toBe('test-999'); |
| 212 | + }); |
| 213 | + |
| 214 | + it('allows rollback after publish', async () => { |
| 215 | + await pg.query(`INSERT INTO test_users (email, name) VALUES ('bob@test.com', 'Bob')`); |
| 216 | + await pg.publish(); // Bob is now committed and cannot be rolled back (important-comment) |
| 217 | + |
| 218 | + await pg.query(`INSERT INTO test_users (email, name) VALUES ('charlie@test.com', 'Charlie')`); |
| 219 | + |
| 220 | + const beforeRollback = await pg.any('SELECT * FROM test_users WHERE email IN ($1, $2)', ['bob@test.com', 'charlie@test.com']); |
| 221 | + expect(beforeRollback.length).toBe(2); |
| 222 | + |
| 223 | + await pg.rollback(); // Rollback Charlie to the savepoint created by publish() (important-comment) |
| 224 | + |
| 225 | + const afterRollback = await pg.any('SELECT * FROM test_users WHERE email IN ($1, $2)', ['bob@test.com', 'charlie@test.com']); |
| 226 | + expect(afterRollback.length).toBe(1); // Only Bob remains (was committed via publish) (important-comment) |
| 227 | + expect(afterRollback[0].email).toBe('bob@test.com'); |
| 228 | + |
| 229 | + // Clean up Bob so it doesn't leak to other tests |
| 230 | + await pg.query(`DELETE FROM test_users WHERE email = 'bob@test.com'`); |
| 231 | + await pg.commit(); // Commit the deletion (important-comment) |
| 232 | + await pg.begin(); // Start new transaction for outer afterEach (important-comment) |
| 233 | + await pg.savepoint(); // Create savepoint for outer afterEach (important-comment) |
| 234 | + }); |
| 235 | +}); |
0 commit comments