import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const { mockQuery, mockConnect } = vi.hoisted(() => ({ mockQuery: vi.fn(), mockConnect: vi.fn(), })); vi.mock('../db/index.js', () => ({ query: mockQuery, getPool: () => ({ connect: mockConnect }), })); import { findByApiKeyHash, findByProviderWorkspace, upsertInstall, updateTokens, getAccessToken, getRefreshToken, getSigningSecret, } from '../db/integrations.dao.js'; import { encryptSecret } from '../lib/crypto.js'; const TEST_KEY = Buffer.alloc(32, 0x2b).toString('base64'); const RAW_ROW = { id: 3, provider: 'slack', external_workspace_id: 'T123', display_name: 'Acme', api_key_hash: 'abc123', access_token_ciphertext: 'iv:tag:payload', refresh_token_ciphertext: 'iv:tag:payload', signing_secret_ciphertext: 'iv:tag:payload', token_expires_at: new Date('2030-01-01T00:00:00.000Z'), scopes: ['channels:history'], }; describe('integrations.dao', () => { let originalKey: string | undefined; beforeEach(() => { vi.clearAllMocks(); originalKey = process.env.TOKEN_ENCRYPTION_KEY; process.env.TOKEN_ENCRYPTION_KEY = TEST_KEY; }); afterEach(() => { if (originalKey === undefined) { delete process.env.TOKEN_ENCRYPTION_KEY; } else { process.env.TOKEN_ENCRYPTION_KEY = originalKey; } }); describe('findByApiKeyHash', () => { it('should return the integration mapped to camelCase fields', async () => { mockQuery.mockResolvedValue({ rows: [RAW_ROW] }); const result = await findByApiKeyHash('abc123'); expect(result).toEqual({ id: 3, provider: 'slack', externalWorkspaceId: 'T123', displayName: 'Acme', scopes: ['channels:history'], tokenExpiresAt: new Date('2030-01-01T00:00:00.000Z'), }); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('WHERE api_key_hash = $1'); expect(params).toEqual(['abc123']); }); it('should not expose any ciphertext or token field on the returned row', async () => { mockQuery.mockResolvedValue({ rows: [RAW_ROW] }); const result = await findByApiKeyHash('abc123'); expect(result).not.toBeNull(); expect(result).not.toHaveProperty('access_token_ciphertext'); expect(result).not.toHaveProperty('refresh_token_ciphertext'); expect(result).not.toHaveProperty('signing_secret_ciphertext'); expect(result).not.toHaveProperty('accessToken'); expect(result).not.toHaveProperty('api_key_hash'); expect(Object.keys(result ?? {}).sort()).toEqual([ 'displayName', 'externalWorkspaceId', 'id', 'provider', 'scopes', 'tokenExpiresAt', ]); }); it('should default scopes to an empty array when the column is null', async () => { mockQuery.mockResolvedValue({ rows: [{ ...RAW_ROW, scopes: null }] }); const result = await findByApiKeyHash('abc123'); expect(result?.scopes).toEqual([]); }); it('should return null when no integration matches the hash', async () => { mockQuery.mockResolvedValue({ rows: [] }); const result = await findByApiKeyHash('nope'); expect(result).toBeNull(); }); }); describe('findByProviderWorkspace', () => { it('should bind the provider and external workspace id', async () => { mockQuery.mockResolvedValue({ rows: [RAW_ROW] }); const result = await findByProviderWorkspace('slack', 'T123'); expect(result?.id).toBe(3); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('WHERE provider = $1 AND external_workspace_id = $2'); expect(params).toEqual(['slack', 'T123']); }); it('should return null when the workspace has no integration', async () => { mockQuery.mockResolvedValue({ rows: [] }); const result = await findByProviderWorkspace('slack', 'T999'); expect(result).toBeNull(); }); }); describe('upsertInstall', () => { it('should encrypt the signing secret before binding it', async () => { mockQuery.mockResolvedValue({ rows: [RAW_ROW] }); await upsertInstall({ provider: 'slack', externalWorkspaceId: 'T123', displayName: 'Acme', signingSecret: 'super-secret', }); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('INSERT INTO integrations'); expect(sql).toContain('ON CONFLICT (provider, external_workspace_id) DO UPDATE SET'); const stored = params[4]; expect(typeof stored).toBe('string'); expect(stored).not.toBe('super-secret'); expect(String(stored).split(':')).toHaveLength(3); }); it('should bind null when no signing secret is supplied', async () => { mockQuery.mockResolvedValue({ rows: [RAW_ROW] }); await upsertInstall({ provider: 'slack', externalWorkspaceId: 'T123' }); const [, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(params[4]).toBeNull(); expect(params[2]).toBeNull(); expect(params[3]).toBeNull(); expect(params[5]).toEqual([]); }); it('should return the public row for the upserted integration', async () => { mockQuery.mockResolvedValue({ rows: [RAW_ROW] }); const result = await upsertInstall({ provider: 'slack', externalWorkspaceId: 'T123' }); expect(result.externalWorkspaceId).toBe('T123'); expect(result).not.toHaveProperty('signing_secret_ciphertext'); }); }); describe('updateTokens', () => { it('should encrypt the access token rather than storing it in plaintext', async () => { mockQuery.mockResolvedValue({ rows: [] }); await updateTokens({ id: 3, accessToken: 'at-plain', refreshToken: 'rt-plain' }); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('access_token_ciphertext = $2'); expect(params[0]).toBe(3); expect(params[1]).not.toBe('at-plain'); expect(String(params[1]).split(':')).toHaveLength(3); expect(params[2]).not.toBe('rt-plain'); }); it('should bind null for an absent refresh token and expiry', async () => { mockQuery.mockResolvedValue({ rows: [] }); await updateTokens({ id: 3, accessToken: 'at-plain' }); const [, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(params[2]).toBeNull(); expect(params[3]).toBeNull(); }); it('should bind the supplied expiry date', async () => { mockQuery.mockResolvedValue({ rows: [] }); const expiresAt = new Date('2031-05-05T10:00:00.000Z'); await updateTokens({ id: 3, accessToken: 'at-plain', expiresAt }); const [, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(params[3]).toEqual(expiresAt); }); }); describe('getAccessToken', () => { it('should decrypt the stored ciphertext back to the original token', async () => { mockQuery.mockResolvedValue({ rows: [{ value: encryptSecret('at-plain') }] }); const result = await getAccessToken(3); expect(result).toBe('at-plain'); const [sql, params] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('SELECT access_token_ciphertext AS value'); expect(params).toEqual([3]); }); it('should return null when no access token is stored', async () => { mockQuery.mockResolvedValue({ rows: [{ value: null }] }); const result = await getAccessToken(3); expect(result).toBeNull(); }); it('should return null when the integration does not exist', async () => { mockQuery.mockResolvedValue({ rows: [] }); const result = await getAccessToken(404); expect(result).toBeNull(); }); }); describe('getRefreshToken', () => { it('should decrypt the refresh token column', async () => { mockQuery.mockResolvedValue({ rows: [{ value: encryptSecret('rt-plain') }] }); const result = await getRefreshToken(3); expect(result).toBe('rt-plain'); const [sql] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('SELECT refresh_token_ciphertext AS value'); }); }); describe('getSigningSecret', () => { it('should decrypt the signing secret column', async () => { mockQuery.mockResolvedValue({ rows: [{ value: encryptSecret('shh') }] }); const result = await getSigningSecret(3); expect(result).toBe('shh'); const [sql] = mockQuery.mock.calls[0] as [string, unknown[]]; expect(sql).toContain('SELECT signing_secret_ciphertext AS value'); }); it('should return null when no signing secret is stored', async () => { mockQuery.mockResolvedValue({ rows: [{ value: null }] }); const result = await getSigningSecret(3); expect(result).toBeNull(); }); }); });