test: mock AnthropicClient in analyze tests to prevent live API calls
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import test, { mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { AnthropicClient } from '../server/domains/shared/adapters/AnthropicClient.js';
|
||||
import { buildApp } from '../server/app.js';
|
||||
import { MockDatabaseConnection } from './helpers/mockDb.js';
|
||||
|
||||
const MOCK_LLM_RESPONSE = JSON.stringify({
|
||||
summary: 'Mocked analysis for test.',
|
||||
sentiment: 'NEUTRAL',
|
||||
affectedIndustries: [],
|
||||
relatedTickers: [],
|
||||
});
|
||||
|
||||
const mockDb = new MockDatabaseConnection() as never;
|
||||
|
||||
test('POST /api/analyze', async (t) => {
|
||||
// Spy on AnthropicClient.prototype.complete before buildApp wires it up.
|
||||
// This prevents any real API calls during tests.
|
||||
const completeSpy = mock.method(
|
||||
AnthropicClient.prototype,
|
||||
'complete',
|
||||
async () => MOCK_LLM_RESPONSE,
|
||||
);
|
||||
|
||||
// Also stub isAvailable so the controller doesn't reject with 400
|
||||
mock.method(AnthropicClient.prototype, 'isAvailable', () => true, { getter: true });
|
||||
|
||||
await t.test('returns analysis when stories match tickers', async () => {
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/analyze',
|
||||
payload: { tickers: ['AAPL'] },
|
||||
});
|
||||
|
||||
// May return no_stories if catalyst cache is empty in test env — that's fine
|
||||
assert.ok(
|
||||
response.statusCode === 200,
|
||||
`Expected 200, got ${response.statusCode}: ${response.body}`,
|
||||
);
|
||||
const body = JSON.parse(response.body);
|
||||
assert.ok('analysis' in body, 'Response should have analysis field');
|
||||
});
|
||||
|
||||
await t.test('returns 400 when ANTHROPIC_API_KEY is missing and no mock', async () => {
|
||||
// Reset the isAvailable mock to simulate no API key
|
||||
mock.method(AnthropicClient.prototype, 'isAvailable', () => false, { getter: true });
|
||||
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/analyze',
|
||||
payload: { tickers: ['AAPL'] },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 400);
|
||||
const body = JSON.parse(response.body);
|
||||
assert.ok(
|
||||
body.error?.includes('ANTHROPIC_API_KEY'),
|
||||
`Expected API key error, got: ${body.error}`,
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('does not call real Anthropic API', async () => {
|
||||
// Restore isAvailable to available
|
||||
mock.method(AnthropicClient.prototype, 'isAvailable', () => true, { getter: true });
|
||||
|
||||
const callsBefore = completeSpy.mock.calls.length;
|
||||
const app = await buildApp({ logger: false, db: mockDb });
|
||||
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/analyze',
|
||||
payload: { tickers: ['NVDA'] },
|
||||
});
|
||||
|
||||
// If complete was called, it used our mock — not the real API
|
||||
const callsAfter = completeSpy.mock.calls.length;
|
||||
if (callsAfter > callsBefore) {
|
||||
// Verify it returned our mock response, not a real API response
|
||||
const lastCall = completeSpy.mock.calls[completeSpy.mock.calls.length - 1];
|
||||
assert.ok(lastCall, 'complete() was called with our spy in place');
|
||||
}
|
||||
// Either way, no real API call was made (spy intercepts)
|
||||
});
|
||||
|
||||
mock.restoreAll();
|
||||
});
|
||||
Reference in New Issue
Block a user