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
This commit is contained in:
Ajax Davis 2026-01-14 02:51:57 +10:00
parent a8a52c972c
commit 907ac2301b
7 changed files with 141 additions and 26 deletions

View file

@ -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..."

View file

@ -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"
},

View file

@ -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();

View file

@ -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');

View file

@ -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;

View file

@ -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);
}
});
});

View file

@ -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,
});
}