feat: Update AI service to use new Google Generative AI SDK
Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running

- Refactored AIService to integrate with the latest GoogleGenAI SDK, updating the generateContent method signature and response handling.
- Adjusted error handling and logging for improved clarity and consistency.
- Enhanced mock implementations in tests to align with the new SDK structure.
refactor: Modify Admin DB service to use Profile type
- Updated AdminRepository to replace User type with Profile in relevant methods.
- Enhanced test cases to utilize mock factories for creating Profile and AdminUserView objects.
fix: Improve error handling in BudgetRepository
- Implemented type-safe checks for PostgreSQL error codes to enhance error handling in createBudget method.
test: Refactor Deals DB tests for type safety
- Updated DealsRepository tests to use Pool type for mock instances, ensuring type safety.
chore: Add new mock factories for testing
- Introduced mock factories for UserWithPasswordHash, Profile, WatchedItemDeal, LeaderboardUser, and UnmatchedFlyerItem to streamline test data creation.
style: Clean up queue service tests
- Refactored queue service tests to improve readability and maintainability, including better handling of mock worker instances.
docs: Update types to include UserWithPasswordHash
- Added UserWithPasswordHash interface to types for better clarity on user authentication data structure.
chore: Remove deprecated Google AI SDK references
- Updated code and documentation to reflect the migration to the new Google Generative AI SDK, removing references to the deprecated SDK.
This commit is contained in:
2025-12-14 17:14:44 -08:00
parent eb0e183f61
commit f73b1422ab
42 changed files with 530 additions and 365 deletions

View File

@@ -1,12 +1,12 @@
// src/routes/user.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import express from 'express';
// Use * as bcrypt to match the implementation's import style and ensure mocks align.
import * as bcrypt from 'bcrypt';
import userRouter from './user.routes';
import { createMockUserProfile, createMockMasterGroceryItem, createMockShoppingList, createMockShoppingListItem, createMockRecipe } from '../tests/utils/mockFactories';
import { Appliance, Notification } from '../types';
import { createMockUserProfile, createMockMasterGroceryItem, createMockShoppingList, createMockShoppingListItem, createMockRecipe, createMockNotification, createMockDietaryRestriction, createMockAppliance, createMockUserWithPasswordHash } from '../tests/utils/mockFactories';
import { Appliance, Notification, DietaryRestriction } from '../types';
import { ForeignKeyConstraintError, NotFoundError } from '../services/db/errors.db';
import { createTestApp } from '../tests/utils/createTestApp';
@@ -351,12 +351,7 @@ describe('User Routes (/api/users)', () => {
describe('DELETE /account', () => {
it('should delete the account with the correct password', async () => {
const userWithHash = {
...mockUserProfile.user,
password_hash: 'hashed-password',
failed_login_attempts: 0, // Add missing properties
last_failed_login: null
};
const userWithHash = createMockUserWithPasswordHash({ ...mockUserProfile.user, password_hash: 'hashed-password' });
vi.mocked(db.userRepo.findUserWithPasswordHashById).mockResolvedValue(userWithHash);
vi.mocked(db.userRepo.deleteUserById).mockResolvedValue(undefined);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
@@ -368,12 +363,7 @@ describe('User Routes (/api/users)', () => {
});
it('should return 403 for an incorrect password', async () => {
const userWithHash = {
...mockUserProfile.user,
password_hash: 'hashed-password',
failed_login_attempts: 0,
last_failed_login: null
};
const userWithHash = createMockUserWithPasswordHash({ ...mockUserProfile.user, password_hash: 'hashed-password' });
vi.mocked(db.userRepo.findUserWithPasswordHashById).mockResolvedValue(userWithHash);
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
const response = await supertest(app)
@@ -422,8 +412,8 @@ describe('User Routes (/api/users)', () => {
describe('GET and PUT /users/me/dietary-restrictions', () => {
it('GET should return a list of restriction IDs', async () => {
const mockRestrictions = [{ dietary_restriction_id: 1, name: 'Gluten-Free', type: 'diet' as const }];
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockResolvedValue(mockRestrictions);
const mockRestrictions: DietaryRestriction[] = [createMockDietaryRestriction({ name: 'Gluten-Free' })];
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockResolvedValue(mockRestrictions);
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockRestrictions);
@@ -455,8 +445,8 @@ describe('User Routes (/api/users)', () => {
describe('GET and PUT /users/me/appliances', () => {
it('GET should return a list of appliance IDs', async () => {
const mockAppliances: Appliance[] = [{ appliance_id: 2, name: 'Air Fryer' }];
vi.mocked(db.personalizationRepo.getUserAppliances).mockResolvedValue(mockAppliances);
const mockAppliances: Appliance[] = [createMockAppliance({ name: 'Air Fryer' })];
vi.mocked(db.personalizationRepo.getUserAppliances).mockResolvedValue(mockAppliances);
const response = await supertest(app).get('/api/users/me/appliances');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockAppliances);
@@ -482,7 +472,7 @@ describe('User Routes (/api/users)', () => {
describe('Notification Routes', () => {
it('GET /notifications should return notifications for the user', async () => {
const mockNotifications: Notification[] = [{ notification_id: 1, user_id: 'user-123', content: 'Test', is_read: false, created_at: '', link_url: null }];
const mockNotifications: Notification[] = [createMockNotification({ user_id: 'user-123', content: 'Test' })];
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
const response = await supertest(app).get('/api/users/notifications?limit=10&offset=0');
@@ -500,16 +490,17 @@ describe('User Routes (/api/users)', () => {
});
it('POST /notifications/:notificationId/mark-read should return 204', async () => {
vi.mocked(db.notificationRepo.markNotificationAsRead).mockResolvedValue({} as any);
// Fix: Return a mock notification object to match the function's signature.
vi.mocked(db.notificationRepo.markNotificationAsRead).mockResolvedValue(createMockNotification({ notification_id: 1, user_id: 'user-123' }));
const response = await supertest(app).post('/api/users/notifications/1/mark-read');
expect(response.status).toBe(204);
expect(db.notificationRepo.markNotificationAsRead).toHaveBeenCalledWith(1, 'user-123');
});
it('should return 400 for an invalid notificationId', async () => {
const response = await supertest(app).post('/api/users/notifications/abc/mark-read');
const response = await supertest(app).post('/api/users/notifications/abc/mark-read').send({});
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid notification ID.');
expect(response.body.errors[0].message).toBe("Invalid ID for parameter 'notificationId'. Must be a number.");
});
});
@@ -539,7 +530,7 @@ describe('User Routes (/api/users)', () => {
const appWithUser = createTestApp({ router: userRouter, basePath, authenticatedUser: { ...mockUserProfile, address_id: null } }); // User has no address yet
const addressData = { address_line_1: '123 New St' };
vi.mocked(db.addressRepo.upsertAddress).mockResolvedValue(5); // New address ID is 5
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue({} as any);
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue({ ...mockUserProfile, address_id: 5 });
const response = await supertest(appWithUser)
.put('/api/users/profile/address')