Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3669958e9d | ||
| 5f3daf0539 | |||
| ae7afaaf97 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -86,12 +86,15 @@ describe('AI Routes (/api/ai)', () => {
|
||||
// Arrange
|
||||
const mkdirError = new Error('EACCES: permission denied');
|
||||
vi.resetModules(); // Reset modules to re-run top-level code
|
||||
vi.doMock('node:fs', () => ({
|
||||
...fs,
|
||||
mkdirSync: vi.fn().mockImplementation(() => {
|
||||
throw mkdirError;
|
||||
}),
|
||||
}));
|
||||
vi.doMock('node:fs', () => {
|
||||
const mockFs = {
|
||||
...fs,
|
||||
mkdirSync: vi.fn().mockImplementation(() => {
|
||||
throw mkdirError;
|
||||
}),
|
||||
};
|
||||
return { ...mockFs, default: mockFs };
|
||||
});
|
||||
const { logger } = await import('../services/logger.server');
|
||||
|
||||
// Act: Dynamically import the router to trigger the mkdirSync call
|
||||
|
||||
@@ -381,7 +381,7 @@ router.post('/logout', async (req: Request, res: Response) => {
|
||||
// Instruct the browser to clear the cookie by setting its expiration to the past.
|
||||
res.cookie('refreshToken', '', {
|
||||
httpOnly: true,
|
||||
expires: new Date(0),
|
||||
maxAge: 0, // Use maxAge for modern compatibility; Express sets 'Expires' as a fallback.
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
res.status(200).json({ message: 'Logged out successfully.' });
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as aiApiClient from './aiApiClient';
|
||||
import { AiAnalysisService } from './aiAnalysisService';
|
||||
import { createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('./aiApiClient');
|
||||
@@ -56,7 +57,7 @@ describe('AiAnalysisService', () => {
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const result = await service.searchWeb([]);
|
||||
const result = await service.searchWeb([createMockFlyerItem({ item: 'test' })]);
|
||||
|
||||
expect(result.text).toBe('Search results');
|
||||
expect(result.sources).toEqual([{ uri: 'https://example.com', title: 'Example' }]);
|
||||
@@ -68,7 +69,7 @@ describe('AiAnalysisService', () => {
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const result = await service.searchWeb([]);
|
||||
const result = await service.searchWeb([createMockFlyerItem({ item: 'test' })]);
|
||||
|
||||
expect(result.text).toBe('Search results');
|
||||
expect(result.sources).toEqual([]);
|
||||
@@ -83,7 +84,7 @@ describe('AiAnalysisService', () => {
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const result = await service.searchWeb([]);
|
||||
const result = await service.searchWeb([createMockFlyerItem({ item: 'test' })]);
|
||||
|
||||
expect(result.sources).toEqual([{ uri: '', title: 'Untitled' }]);
|
||||
});
|
||||
@@ -92,7 +93,9 @@ describe('AiAnalysisService', () => {
|
||||
const apiError = new Error('API is down');
|
||||
vi.mocked(aiApiClient.searchWeb).mockRejectedValue(apiError);
|
||||
|
||||
await expect(service.searchWeb([])).rejects.toThrow(apiError);
|
||||
await expect(service.searchWeb([createMockFlyerItem({ item: 'test' })])).rejects.toThrow(
|
||||
apiError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -42,9 +42,11 @@ export class AiAnalysisService {
|
||||
*/
|
||||
async searchWeb(items: FlyerItem[]): Promise<GroundedResponse> {
|
||||
logger.info('[AiAnalysisService] searchWeb called.');
|
||||
// Construct a query string from the item names.
|
||||
const query = items.map((item) => item.item).join(', ');
|
||||
// The API client returns a specific shape that we need to await the JSON from
|
||||
const response: { text: string; sources: RawSource[] } = await aiApiClient
|
||||
.searchWeb(items)
|
||||
.searchWeb(query)
|
||||
.then((res) => res.json());
|
||||
// Normalize sources to a consistent format.
|
||||
const mappedSources = (response.sources || []).map(
|
||||
|
||||
@@ -282,15 +282,15 @@ describe('AI API Client (Network Mocking with MSW)', () => {
|
||||
});
|
||||
|
||||
describe('searchWeb', () => {
|
||||
it('should send items as JSON in the body', async () => {
|
||||
const items = [createMockFlyerItem({ item: 'search me' })];
|
||||
await aiApiClient.searchWeb(items, undefined, 'test-token');
|
||||
it('should send query as JSON in the body', async () => {
|
||||
const query = 'search me';
|
||||
await aiApiClient.searchWeb(query, undefined, 'test-token');
|
||||
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
const req = requestSpy.mock.calls[0][0];
|
||||
|
||||
expect(req.endpoint).toBe('search-web');
|
||||
expect(req.body).toEqual({ items });
|
||||
expect(req.body).toEqual({ query });
|
||||
expect(req.headers.get('Authorization')).toBe('Bearer test-token');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ export const getDeepDiveAnalysis = async (
|
||||
};
|
||||
|
||||
export const searchWeb = async (
|
||||
items: Partial<FlyerItem>[],
|
||||
query: string,
|
||||
signal?: AbortSignal,
|
||||
tokenOverride?: string,
|
||||
): Promise<Response> => {
|
||||
@@ -144,7 +144,7 @@ export const searchWeb = async (
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items }),
|
||||
body: JSON.stringify({ query }),
|
||||
signal,
|
||||
},
|
||||
{ tokenOverride, signal },
|
||||
|
||||
@@ -624,14 +624,10 @@ describe('User DB Service', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError if token is not found', async () => {
|
||||
it('should return undefined if token is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
|
||||
await expect(userRepo.findUserByRefreshToken('a-token', mockLogger)).rejects.toThrow(
|
||||
NotFoundError,
|
||||
);
|
||||
await expect(userRepo.findUserByRefreshToken('a-token', mockLogger)).rejects.toThrow(
|
||||
'User not found for the given refresh token.',
|
||||
);
|
||||
const result = await userRepo.findUserByRefreshToken('a-token', mockLogger);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
|
||||
@@ -434,23 +434,21 @@ export class UserRepository {
|
||||
* @param refreshToken The refresh token to look up.
|
||||
* @returns A promise that resolves to the user object (id, email) or undefined if not found.
|
||||
*/
|
||||
// prettier-ignore
|
||||
async findUserByRefreshToken(refreshToken: string, logger: Logger): Promise<{ user_id: string; email: string; }> {
|
||||
async findUserByRefreshToken(
|
||||
refreshToken: string,
|
||||
logger: Logger,
|
||||
): Promise<{ user_id: string; email: string } | undefined> {
|
||||
try {
|
||||
const res = await this.db.query<{ user_id: string; email: string }>(
|
||||
'SELECT user_id, email FROM public.users WHERE refresh_token = $1',
|
||||
[refreshToken]
|
||||
[refreshToken],
|
||||
);
|
||||
if ((res.rowCount ?? 0) === 0) {
|
||||
throw new NotFoundError('User not found for the given refresh token.');
|
||||
return undefined;
|
||||
}
|
||||
return res.rows[0];
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) throw error;
|
||||
logger.error(
|
||||
{ err: error },
|
||||
'Database error in findUserByRefreshToken',
|
||||
);
|
||||
logger.error({ err: error }, 'Database error in findUserByRefreshToken');
|
||||
throw new Error('Failed to find user by refresh token.'); // Generic error for other failures
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/tests/integration/admin.integration.test.ts
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import { getPool } from '../../services/db/connection.db';
|
||||
import type { UserProfile } from '../../types';
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('AI API Routes Integration Tests', () => {
|
||||
const mockImageFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
||||
const response = await aiApiClient.extractAddressFromImage(mockImageFile, authToken);
|
||||
const result = await response.json();
|
||||
expect(result.address).toBe('123 AI Street, Server City');
|
||||
expect(result.address).toBe('not identified');
|
||||
});
|
||||
|
||||
it('POST /api/ai/extract-logo should return a stubbed response', async () => {
|
||||
@@ -66,24 +66,28 @@ describe('AI API Routes Integration Tests', () => {
|
||||
});
|
||||
|
||||
it('POST /api/ai/quick-insights should return a stubbed insight', async () => {
|
||||
const response = await aiApiClient.getQuickInsights([], undefined, authToken);
|
||||
const response = await aiApiClient.getQuickInsights([{ item: 'test' }], undefined, authToken);
|
||||
const result = await response.json();
|
||||
expect(result.text).toBe('This is a server-generated quick insight: buy the cheap stuff!');
|
||||
});
|
||||
|
||||
it('POST /api/ai/deep-dive should return a stubbed analysis', async () => {
|
||||
const response = await aiApiClient.getDeepDiveAnalysis([], undefined, authToken);
|
||||
const response = await aiApiClient.getDeepDiveAnalysis(
|
||||
[{ item: 'test' }],
|
||||
undefined,
|
||||
authToken,
|
||||
);
|
||||
const result = await response.json();
|
||||
expect(result.text).toBe('This is a server-generated deep dive analysis. It is very detailed.');
|
||||
});
|
||||
|
||||
it('POST /api/ai/search-web should return a stubbed search result', async () => {
|
||||
const response = await aiApiClient.searchWeb([], undefined, authToken);
|
||||
const response = await aiApiClient.searchWeb('test query', undefined, authToken);
|
||||
const result = await response.json();
|
||||
expect(result).toEqual({ text: 'The web says this is good.', sources: [] });
|
||||
});
|
||||
|
||||
it('POST /api/ai/plan-trip should return a stubbed trip plan', async () => {
|
||||
it('POST /api/ai/plan-trip should return an error as the feature is disabled', async () => {
|
||||
// The GeolocationCoordinates type requires more than just lat/lng.
|
||||
// We create a complete mock object to satisfy the type.
|
||||
const mockLocation: TestGeolocationCoordinates = {
|
||||
@@ -103,11 +107,11 @@ describe('AI API Routes Integration Tests', () => {
|
||||
undefined,
|
||||
authToken,
|
||||
);
|
||||
const result = await response.json();
|
||||
expect(result).toBeDefined();
|
||||
// The AI service is mocked in unit tests, but in integration it might be live.
|
||||
// For now, we just check that we get a text response.
|
||||
expect(result.text).toBeTypeOf('string');
|
||||
// The service for this endpoint is disabled and throws an error, which results in a 500.
|
||||
expect(response.ok).toBe(false);
|
||||
expect(response.status).toBe(500);
|
||||
const errorResult = await response.json();
|
||||
expect(errorResult.message).toContain('planTripWithMaps');
|
||||
});
|
||||
|
||||
it('POST /api/ai/generate-image should reject because it is not implemented', async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Public API Routes Integration Tests', () => {
|
||||
// which also handles activity logging correctly.
|
||||
const { user: createdUser } = await createAndLoginUser({
|
||||
email: userEmail,
|
||||
password: 'test-hash',
|
||||
password: 'a-Very-Strong-Password-123!',
|
||||
fullName: 'Public Routes Test User',
|
||||
});
|
||||
testUser = createdUser;
|
||||
|
||||
Reference in New Issue
Block a user