From 907ac2301b7f1665b96429a6c25825d9be5106cf Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Wed, 14 Jan 2026 02:51:57 +1000 Subject: [PATCH] fix: fix remaining integration test issues - Fix user-profile tests to use apiKeyClient instead of session auth - Fix stats test for new nested response structure (data.overview.*) - Enable conversation tests with CI OpenAI key setup - Add setup-openai-key.ts script to configure test user's OPENAI_API_KEY - Update workflow to run OpenAI key setup before tests --- .github/workflows/integration-tests.yml | 8 ++ apps/web/package.json | 1 + .../integration/_helpers/setup-openai-key.ts | 78 +++++++++++++++++++ .../_helpers/setup-test-credentials.ts | 34 +++++++- .../agents-conversations.integration.test.ts | 18 ++--- .../stats/stats.integration.test.ts | 18 +++-- .../user/user-profile.integration.test.ts | 10 +-- 7 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 apps/web/src/test/integration/_helpers/setup-openai-key.ts diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b62c4cc..ec7401f 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -50,6 +50,14 @@ jobs: INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }} run: pnpm --filter=@tpmjs/web test:cleanup-orphans + - name: Setup OpenAI key for test user + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + API_KEY_ENCRYPTION_SECRET: ${{ secrets.API_KEY_ENCRYPTION_SECRET }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + INTEGRATION_TEST_USER_ID: ${{ secrets.INTEGRATION_TEST_USER_ID }} + run: pnpm --filter=@tpmjs/web test:setup-openai-key + - name: Wait for API run: | echo "Checking if API is available at $TEST_BASE_URL..." diff --git a/apps/web/package.json b/apps/web/package.json index c7b831c..7005c18 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "test:integration": "INTEGRATION_TESTS=true vitest run --config vitest.integration.config.ts", "test:integration:watch": "INTEGRATION_TESTS=true vitest --config vitest.integration.config.ts", "test:setup-credentials": "tsx src/test/integration/_helpers/setup-test-credentials.ts", + "test:setup-openai-key": "tsx src/test/integration/_helpers/setup-openai-key.ts", "test:cleanup-orphans": "tsx src/test/integration/_helpers/cleanup-orphans.ts", "generate-og": "tsx scripts/generate-og-images.ts" }, diff --git a/apps/web/src/test/integration/_helpers/setup-openai-key.ts b/apps/web/src/test/integration/_helpers/setup-openai-key.ts new file mode 100644 index 0000000..aefc919 --- /dev/null +++ b/apps/web/src/test/integration/_helpers/setup-openai-key.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env tsx +/** + * Setup script to store OPENAI_API_KEY for integration tests + * + * This script should be run in CI before the integration tests to ensure + * the test user has an OPENAI_API_KEY configured for agent conversation tests. + * + * Required environment variables: + * - DATABASE_URL: Neon PostgreSQL connection string + * - API_KEY_ENCRYPTION_SECRET: Encryption secret for API keys + * - OPENAI_API_KEY: The OpenAI API key to store + * - INTEGRATION_TEST_USER_ID: The test user's ID + */ + +import { resolve } from 'node:path'; +import { config } from 'dotenv'; + +// Load environment variables +config({ path: resolve(__dirname, '../../../../.env.local') }); +config({ path: resolve(__dirname, '../../../../.env') }); + +// Validate required env vars +const requiredEnvVars = [ + 'DATABASE_URL', + 'API_KEY_ENCRYPTION_SECRET', + 'OPENAI_API_KEY', + 'INTEGRATION_TEST_USER_ID', +]; + +for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + console.log(`Skipping: ${envVar} not set`); + process.exit(0); // Exit gracefully - tests that need this will be skipped + } +} + +// Import after env validation +import { prisma } from '@tpmjs/db'; +import { encryptApiKey } from '@/lib/crypto/api-keys'; + +async function main() { + const userId = process.env.INTEGRATION_TEST_USER_ID!; + const openaiKey = process.env.OPENAI_API_KEY!; + + console.log('Setting up OPENAI_API_KEY for integration test user...'); + + try { + // Delete existing key if any + await prisma.userApiKey.deleteMany({ + where: { + userId, + keyName: 'OPENAI_API_KEY', + }, + }); + + // Encrypt and store the key + const { encrypted, iv } = encryptApiKey(openaiKey); + await prisma.userApiKey.create({ + data: { + userId, + keyName: 'OPENAI_API_KEY', + encryptedKey: encrypted, + keyIv: iv, + keyHint: openaiKey.slice(-4), + }, + }); + + console.log('OPENAI_API_KEY configured for test user'); + } catch (error) { + console.error('Failed to setup OPENAI_API_KEY:', error); + // Don't fail the build - tests will be skipped if key isn't available + process.exit(0); + } finally { + await prisma.$disconnect(); + } +} + +main(); diff --git a/apps/web/src/test/integration/_helpers/setup-test-credentials.ts b/apps/web/src/test/integration/_helpers/setup-test-credentials.ts index 59de7d7..c8c6e0b 100644 --- a/apps/web/src/test/integration/_helpers/setup-test-credentials.ts +++ b/apps/web/src/test/integration/_helpers/setup-test-credentials.ts @@ -117,7 +117,39 @@ async function main() { }); console.log(' API key created'); - // 4. Output credentials + // 4. Store OPENAI_API_KEY for agent conversations (if available) + const openaiKey = process.env.OPENAI_API_KEY; + if (openaiKey) { + console.log('\nšŸ“ Storing OPENAI_API_KEY for agent conversations...'); + + // Import encryption function + const { encryptApiKey } = await import('@/lib/crypto/api-keys'); + + // Delete existing key if any + await prisma.userApiKey.deleteMany({ + where: { + userId: user.id, + keyName: 'OPENAI_API_KEY', + }, + }); + + // Encrypt and store the key + const { encrypted, iv } = encryptApiKey(openaiKey); + await prisma.userApiKey.create({ + data: { + userId: user.id, + keyName: 'OPENAI_API_KEY', + encryptedKey: encrypted, + keyIv: iv, + keyHint: openaiKey.slice(-4), + }, + }); + console.log(' OPENAI_API_KEY stored'); + } else { + console.log('\nāš ļø OPENAI_API_KEY not set - agent conversation tests will be skipped'); + } + + // 5. Output credentials console.log('\n' + '='.repeat(60)); console.log('šŸŽ‰ Integration test credentials generated!\n'); console.log('Add these to your .env.local or GitHub secrets:\n'); diff --git a/apps/web/src/test/integration/agents/agents-conversations.integration.test.ts b/apps/web/src/test/integration/agents/agents-conversations.integration.test.ts index 8e2b143..a2ec920 100644 --- a/apps/web/src/test/integration/agents/agents-conversations.integration.test.ts +++ b/apps/web/src/test/integration/agents/agents-conversations.integration.test.ts @@ -3,10 +3,8 @@ * * Tests the agent chat/conversation endpoints with streaming responses. * - * NOTE: Tests that make actual AI calls are skipped because they require - * the test user to have AI provider API keys (OPENAI_API_KEY, etc.) configured. - * These tests would work in a fully configured test environment but are not - * suitable for CI/CD against production databases. + * NOTE: These tests require the test user to have OPENAI_API_KEY configured + * in the database. The CI workflow sets this up via test:setup-openai-key. */ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -34,8 +32,7 @@ describe('Agent Conversations Endpoints', () => { }); describe('POST /api/:username/agents/:uid/conversation/:conversationId', () => { - // Skip: Requires AI provider API keys configured for the test user - it.skip('should create a conversation and receive streaming response', async () => { + it('should create a conversation and receive streaming response', async () => { if (!testAgent) { console.log('Skipping: No test agent available'); return; @@ -69,8 +66,7 @@ describe('Agent Conversations Endpoints', () => { } }); - // Skip: Requires AI provider API keys configured for the test user - it.skip('should maintain conversation context on follow-up messages', async () => { + it('should maintain conversation context on follow-up messages', async () => { if (!testAgent) { console.log('Skipping: No test agent available'); return; @@ -117,8 +113,7 @@ describe('Agent Conversations Endpoints', () => { }); describe('GET /api/:username/agents/:uid/conversation/:conversationId', () => { - // Skip: Requires creating a conversation first, which needs AI provider API keys - it.skip('should retrieve conversation history', async () => { + it('should retrieve conversation history', async () => { if (!testAgent) { console.log('Skipping: No test agent available'); return; @@ -170,8 +165,7 @@ describe('Agent Conversations Endpoints', () => { }); describe('GET /api/:username/agents/:uid/conversations', () => { - // Skip: Requires creating a conversation first, which needs AI provider API keys - it.skip('should list all conversations for an agent', async () => { + it('should list all conversations for an agent', async () => { if (!testAgent) { console.log('Skipping: No test agent available'); return; diff --git a/apps/web/src/test/integration/stats/stats.integration.test.ts b/apps/web/src/test/integration/stats/stats.integration.test.ts index b442b24..d8b0da0 100644 --- a/apps/web/src/test/integration/stats/stats.integration.test.ts +++ b/apps/web/src/test/integration/stats/stats.integration.test.ts @@ -23,12 +23,14 @@ describe('Stats Endpoints', () => { const result = await ctx.publicClient.get<{ success: boolean; data: { - totalTools: number; - totalPackages: number; - officialTools: number; - toolsWithSchema: number; - healthyTools: number; - brokenTools: number; + overview: { + totalTools: number; + totalPackages: number; + officialTools: number; + }; + health: { + import: { healthy: number; broken: number }; + }; }; }>('/api/stats'); @@ -36,8 +38,8 @@ describe('Stats Endpoints', () => { if (result.ok) { expect(result.data.success).toBe(true); expect(result.data.data).toBeDefined(); - expect(result.data.data.totalTools).toBeGreaterThanOrEqual(0); - expect(result.data.data.totalPackages).toBeGreaterThanOrEqual(0); + expect(result.data.data.overview.totalTools).toBeGreaterThanOrEqual(0); + expect(result.data.data.overview.totalPackages).toBeGreaterThanOrEqual(0); } }); }); diff --git a/apps/web/src/test/integration/user/user-profile.integration.test.ts b/apps/web/src/test/integration/user/user-profile.integration.test.ts index 7b4352a..f4bcd83 100644 --- a/apps/web/src/test/integration/user/user-profile.integration.test.ts +++ b/apps/web/src/test/integration/user/user-profile.integration.test.ts @@ -28,8 +28,8 @@ describe('User Profile Endpoints', () => { }); describe('GET /api/user/profile', () => { - it('should return user profile with session auth', async () => { - const result = await ctx.api.get<{ + it('should return user profile with API key auth', async () => { + const result = await ctx.apiKeyClient.get<{ success: boolean; data: UserProfile; }>('/api/user/profile'); @@ -54,7 +54,7 @@ describe('User Profile Endpoints', () => { describe('PATCH /api/user/profile', () => { it('should update user profile', async () => { // First, get current profile - const profileResult = await ctx.api.get<{ + const profileResult = await ctx.apiKeyClient.get<{ success: boolean; data: UserProfile; }>('/api/user/profile'); @@ -65,7 +65,7 @@ describe('User Profile Endpoints', () => { const originalName = profileResult.data.data.name; // Update name - const updateResult = await ctx.api.patch<{ + const updateResult = await ctx.apiKeyClient.patch<{ success: boolean; data: UserProfile; }>('/api/user/profile', { @@ -78,7 +78,7 @@ describe('User Profile Endpoints', () => { } // Restore original name - await ctx.api.patch('/api/user/profile', { + await ctx.apiKeyClient.patch('/api/user/profile', { name: originalName, }); }