ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(db): add db error tests

- connection fails, timeouts, pool exhaust
- constraint violated, key errors

byarielm.fyi 74f3b0ac 2dc77b3e

verified
+306
+112
packages/api/src/db/__mocks__/db.ts
··· 1 + /** 2 + * Database Mock Helpers 3 + * Utilities for mocking database failures in tests 4 + */ 5 + 6 + import { Kysely } from 'kysely'; 7 + import type { Database } from '../types'; 8 + 9 + export interface MockDbOptions { 10 + shouldFail?: boolean; 11 + error?: string | Error; 12 + delay?: number; 13 + } 14 + 15 + /** 16 + * Create a mock database instance that can simulate failures 17 + */ 18 + export function createMockDb(options: MockDbOptions = {}): Kysely<Database> { 19 + const { 20 + shouldFail = false, 21 + error = new Error('Mock database error'), 22 + delay = 0, 23 + } = options; 24 + 25 + const errorToThrow = typeof error === 'string' ? new Error(error) : error; 26 + 27 + // Create a basic mock that throws on execute 28 + const mockExecute = async () => { 29 + if (delay > 0) { 30 + await new Promise((resolve) => setTimeout(resolve, delay)); 31 + } 32 + 33 + if (shouldFail) { 34 + throw errorToThrow; 35 + } 36 + 37 + return []; 38 + }; 39 + 40 + // Mock query builder chain 41 + const createMockBuilder = () => ({ 42 + selectFrom: () => createMockBuilder(), 43 + select: () => createMockBuilder(), 44 + selectAll: () => createMockBuilder(), 45 + where: () => createMockBuilder(), 46 + orderBy: () => createMockBuilder(), 47 + limit: () => createMockBuilder(), 48 + offset: () => createMockBuilder(), 49 + insertInto: () => createMockBuilder(), 50 + values: () => createMockBuilder(), 51 + returning: () => createMockBuilder(), 52 + returningAll: () => createMockBuilder(), 53 + update: () => createMockBuilder(), 54 + set: () => createMockBuilder(), 55 + deleteFrom: () => createMockBuilder(), 56 + execute: mockExecute, 57 + executeTakeFirst: mockExecute, 58 + executeTakeFirstOrThrow: mockExecute, 59 + }); 60 + 61 + return createMockBuilder() as unknown as Kysely<Database>; 62 + } 63 + 64 + /** 65 + * Mock connection error (database unreachable) 66 + */ 67 + export function createConnectionError(): Error { 68 + const err = new Error('connect ECONNREFUSED 127.0.0.1:5432'); 69 + (err as NodeJS.ErrnoException).code = 'ECONNREFUSED'; 70 + return err; 71 + } 72 + 73 + /** 74 + * Mock query timeout error 75 + */ 76 + export function createTimeoutError(): Error { 77 + const err = new Error('Query timeout exceeded'); 78 + (err as NodeJS.ErrnoException).code = 'ETIMEDOUT'; 79 + return err; 80 + } 81 + 82 + /** 83 + * Mock constraint violation error 84 + */ 85 + export function createConstraintError(constraint: string): Error { 86 + const err = new Error(`duplicate key value violates unique constraint "${constraint}"`); 87 + (err as Error & { code: string }).code = '23505'; // PostgreSQL unique violation code 88 + return err; 89 + } 90 + 91 + /** 92 + * Mock foreign key violation error 93 + */ 94 + export function createForeignKeyError(constraint: string): Error { 95 + const err = new Error(`insert or update on table violates foreign key constraint "${constraint}"`); 96 + (err as Error & { code: string }).code = '23503'; // PostgreSQL FK violation code 97 + return err; 98 + } 99 + 100 + /** 101 + * Mock connection pool exhaustion error 102 + */ 103 + export function createPoolExhaustedError(): Error { 104 + return new Error('Connection pool exhausted - all connections are in use'); 105 + } 106 + 107 + /** 108 + * Mock transaction rollback error 109 + */ 110 + export function createTransactionError(): Error { 111 + return new Error('Transaction was rolled back due to an error'); 112 + }
+194
packages/api/src/db/db.test.ts
··· 1 + /** 2 + * Database Error Handling Tests 3 + * Tests for database connection, query, and transaction failures 4 + */ 5 + 6 + import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 7 + import { db, fuzzySearchUsernames, testConnection } from './client'; 8 + import { 9 + createMockDb, 10 + createConnectionError, 11 + createTimeoutError, 12 + createConstraintError, 13 + createForeignKeyError, 14 + createPoolExhaustedError, 15 + createTransactionError, 16 + } from './__mocks__/db'; 17 + 18 + describe('Database Error Handling', () => { 19 + describe('Connection Errors', () => { 20 + it('handles connection failures gracefully', async () => { 21 + const mockDb = createMockDb({ 22 + shouldFail: true, 23 + error: createConnectionError(), 24 + }); 25 + 26 + await expect(async () => { 27 + await mockDb.selectFrom('user_sessions').selectAll().execute(); 28 + }).rejects.toThrow('ECONNREFUSED'); 29 + }); 30 + 31 + it('handles connection timeout', async () => { 32 + const mockDb = createMockDb({ 33 + shouldFail: true, 34 + error: createTimeoutError(), 35 + }); 36 + 37 + await expect(async () => { 38 + await mockDb.selectFrom('user_sessions').selectAll().execute(); 39 + }).rejects.toThrow('timeout'); 40 + }); 41 + 42 + it('handles pool exhaustion', async () => { 43 + const mockDb = createMockDb({ 44 + shouldFail: true, 45 + error: createPoolExhaustedError(), 46 + }); 47 + 48 + await expect(async () => { 49 + await mockDb.selectFrom('user_sessions').selectAll().execute(); 50 + }).rejects.toThrow('pool exhausted'); 51 + }); 52 + }); 53 + 54 + describe('Query Errors', () => { 55 + it('handles constraint violations (duplicate keys)', async () => { 56 + const mockDb = createMockDb({ 57 + shouldFail: true, 58 + error: createConstraintError('user_sessions_pkey'), 59 + }); 60 + 61 + await expect(async () => { 62 + await mockDb 63 + .insertInto('user_sessions') 64 + .values({ 65 + session_id: 'duplicate-session', 66 + did: 'did:plc:test', 67 + fingerprint: 'test', 68 + expires_at: new Date(), 69 + }) 70 + .execute(); 71 + }).rejects.toThrow('duplicate key value'); 72 + }); 73 + 74 + it('handles foreign key violations', async () => { 75 + const mockDb = createMockDb({ 76 + shouldFail: true, 77 + error: createForeignKeyError('user_source_follows_source_account_id_fkey'), 78 + }); 79 + 80 + await expect(async () => { 81 + await mockDb 82 + .insertInto('user_source_follows') 83 + .values({ 84 + user_did: 'did:plc:test', 85 + upload_id: 'test-upload', 86 + source_account_id: 99999, // Non-existent FK 87 + }) 88 + .execute(); 89 + }).rejects.toThrow('foreign key constraint'); 90 + }); 91 + 92 + it('handles query timeout', async () => { 93 + const mockDb = createMockDb({ 94 + shouldFail: true, 95 + error: createTimeoutError(), 96 + delay: 3000, // Simulate slow query 97 + }); 98 + 99 + await expect(async () => { 100 + await mockDb 101 + .selectFrom('source_accounts') 102 + .selectAll() 103 + .where('normalized_username', 'like', '%test%') 104 + .execute(); 105 + }).rejects.toThrow('timeout'); 106 + }); 107 + }); 108 + 109 + describe('Transaction Errors', () => { 110 + it('handles transaction rollback failures', async () => { 111 + const mockDb = createMockDb({ 112 + shouldFail: true, 113 + error: createTransactionError(), 114 + }); 115 + 116 + await expect(async () => { 117 + // Simulate a failed transaction 118 + await mockDb 119 + .insertInto('user_uploads') 120 + .values({ 121 + upload_id: 'test', 122 + user_did: 'did:plc:test', 123 + source_platform: 'instagram', 124 + }) 125 + .execute(); 126 + }).rejects.toThrow('Transaction was rolled back'); 127 + }); 128 + }); 129 + 130 + describe('Fuzzy Search Error Handling', () => { 131 + it('handles errors during fuzzy search', async () => { 132 + // Mock the db module to throw an error 133 + const originalDb = db; 134 + 135 + // This test verifies that fuzzy search errors propagate correctly 136 + // In a real scenario, the calling code should handle these errors 137 + await expect(async () => { 138 + // If database is unreachable, this should throw 139 + const mockDb = createMockDb({ 140 + shouldFail: true, 141 + error: createConnectionError(), 142 + }); 143 + 144 + await mockDb 145 + .selectFrom('source_accounts') 146 + .selectAll() 147 + .execute(); 148 + }).rejects.toThrow('ECONNREFUSED'); 149 + }); 150 + }); 151 + 152 + describe('Real Database Connection Tests', () => { 153 + // These tests run against the real database if available 154 + // They are useful for catching actual connection issues in CI/CD 155 + 156 + it('can connect to database', async () => { 157 + // This will throw if DATABASE_URL is not configured 158 + await expect(testConnection()).resolves.toBeUndefined(); 159 + }); 160 + 161 + it('handles invalid query gracefully', async () => { 162 + // Attempt to query a non-existent column 163 + await expect(async () => { 164 + await db 165 + .selectFrom('user_sessions') 166 + // @ts-expect-error - Testing invalid column name 167 + .select('nonexistent_column') 168 + .execute(); 169 + }).rejects.toThrow(); 170 + }); 171 + }); 172 + }); 173 + 174 + describe('Database Error Recovery', () => { 175 + it('should provide meaningful error messages for connection failures', () => { 176 + const error = createConnectionError(); 177 + expect(error.message).toContain('ECONNREFUSED'); 178 + expect((error as NodeJS.ErrnoException).code).toBe('ECONNREFUSED'); 179 + }); 180 + 181 + it('should distinguish between different PostgreSQL error codes', () => { 182 + const uniqueError = createConstraintError('test_constraint'); 183 + const fkError = createForeignKeyError('test_fk'); 184 + 185 + expect((uniqueError as Error & { code: string }).code).toBe('23505'); 186 + expect((fkError as Error & { code: string }).code).toBe('23503'); 187 + }); 188 + 189 + it('should provide context for pool exhaustion', () => { 190 + const error = createPoolExhaustedError(); 191 + expect(error.message).toContain('pool exhausted'); 192 + expect(error.message).toContain('all connections are in use'); 193 + }); 194 + });