Add user database service and unit tests
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m28s

- Implement user database service with functions for user management (create, find, update, delete).
- Add comprehensive unit tests for user database service using Vitest.
- Mock database interactions to ensure isolated testing.
- Create setup files for unit tests to handle database connections and global mocks.
- Introduce error handling for unique constraints and foreign key violations.
- Enhance logging for better traceability during database operations.
This commit is contained in:
2025-12-04 15:30:27 -08:00
parent 4cf587c8f0
commit 80d2b1ffe6
62 changed files with 135 additions and 136 deletions

View File

@@ -1,5 +1,5 @@
// src/services/db/address.ts
import { getPool } from './connection';
// src/services/db/address.db.ts
import { getPool } from './connection.db';
import { logger } from '../logger.server';
import { Address } from '../../types';

View File

@@ -1,4 +1,4 @@
// src/services/db/admin.test.ts
// src/services/db/admin.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getSuggestedCorrections,
@@ -15,8 +15,8 @@ import {
getUnmatchedFlyerItems,
updateRecipeStatus,
updateReceiptStatus,
} from './admin';
import { getPool } from './connection';
} from './admin.db';
import { getPool } from './connection.db';
import type { SuggestedCorrection } from '../../types';
// Define test-local mock functions. These will be used to control the mock's behavior.

View File

@@ -1,6 +1,6 @@
// src/services/db/admin.ts
import { getPool } from './connection';
import { ForeignKeyConstraintError } from './errors';
// src/services/db/admin.db.ts
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { logger } from '../logger';
import { SuggestedCorrection, MostFrequentSaleItem, Recipe, RecipeComment, UnmatchedFlyerItem, ActivityLogItem, Receipt, User } from '../../types';
/**

View File

@@ -1,4 +1,4 @@
// src/services/db/budget.test.ts
// src/services/db/budget.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getBudgetsForUser,
@@ -6,7 +6,7 @@ import {
updateBudget,
deleteBudget,
getSpendingByCategory,
} from './budget';
} from './budget.db';
import { Pool } from 'pg';
const mockQuery = vi.fn();

View File

@@ -1,6 +1,6 @@
// src/services/db/budget.ts
import { getPool } from './connection';
import { ForeignKeyConstraintError } from './errors';
// src/services/db/budget.db.ts
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { logger } from '../logger';
import { Budget, SpendingByCategory } from '../../types';

View File

@@ -1,4 +1,4 @@
// src/services/db/connection.test.ts
// src/services/db/connection.db.test.ts
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
// Mock the logger
@@ -12,37 +12,37 @@ vi.mock('../logger', () => ({
describe('DB Connection Service', () => {
beforeEach(async () => {
vi.clearAllMocks();
// We reset modules to ensure 'connection.ts' re-evaluates and calls the (mocked) Pool constructor again
// We reset modules to ensure 'connection.db.ts' re-evaluates and calls the (mocked) Pool constructor again
vi.resetModules();
});
// --- DIAGNOSTIC TEST ---
it('DEBUG: Verify pg import and Pool constructor', async () => {
const pg = await import('pg');
console.log('[DEBUG] connection.test.ts: keys(pg):', Object.keys(pg));
console.log('[DEBUG] connection.test.ts: typeof pg.Pool:', typeof pg.Pool);
console.log('[DEBUG] connection.db.test.ts: keys(pg):', Object.keys(pg));
console.log('[DEBUG] connection.db.test.ts: typeof pg.Pool:', typeof pg.Pool);
if (typeof pg.Pool === 'function') {
try {
const instance = new pg.Pool();
console.log('[DEBUG] connection.test.ts: new pg.Pool() SUCCESS. Instance:', Object.keys(instance));
console.log('[DEBUG] connection.db.test.ts: new pg.Pool() SUCCESS. Instance:', Object.keys(instance));
} catch (e) {
console.error('[DEBUG] connection.test.ts: new pg.Pool() FAILED:', e);
console.error('[DEBUG] connection.db.test.ts: new pg.Pool() FAILED:', e);
}
} else {
console.error('[DEBUG] connection.test.ts: pg.Pool is NOT a function!');
console.error('[DEBUG] connection.db.test.ts: pg.Pool is NOT a function!');
}
});
describe('getPool', () => {
it('should create a new pool instance on the first call', async () => {
// Re-import to ensure fresh state after resetModules
const { getPool } = await import('./connection');
const { getPool } = await import('./connection.db');
const { Pool } = await import('pg'); // Get the current mock
const pool = getPool();
console.log('[DEBUG] connection.test.ts: getPool() returned:', pool ? 'Object' : pool);
console.log('[DEBUG] connection.db.test.ts: getPool() returned:', pool ? 'Object' : pool);
// Verify Pool constructor was called
expect(Pool).toHaveBeenCalledTimes(1);
@@ -53,7 +53,7 @@ describe('DB Connection Service', () => {
});
it('should return the same pool instance on subsequent calls', async () => {
const { getPool } = await import('./connection');
const { getPool } = await import('./connection.db');
const { Pool } = await import('pg');
const pool1 = getPool();
@@ -66,7 +66,7 @@ describe('DB Connection Service', () => {
describe('checkTablesExist', () => {
it('should return an empty array if all tables exist', async () => {
const { getPool, checkTablesExist } = await import('./connection');
const { getPool, checkTablesExist } = await import('./connection.db');
const pool = getPool();
// Mock the query response on the *instance* returned by getPool
@@ -81,7 +81,7 @@ describe('DB Connection Service', () => {
});
it('should return an array of missing tables', async () => {
const { getPool, checkTablesExist } = await import('./connection');
const { getPool, checkTablesExist } = await import('./connection.db');
const pool = getPool();
(pool.query as Mock).mockResolvedValue({ rows: [{ table_name: 'users' }] });
@@ -95,7 +95,7 @@ describe('DB Connection Service', () => {
describe('getPoolStatus', () => {
it('should return the status counts from the pool instance', async () => {
const { getPoolStatus } = await import('./connection');
const { getPoolStatus } = await import('./connection.db');
const status = getPoolStatus();
// These values match the default mock implementation in unit-setup/mock-db

View File

@@ -1,4 +1,4 @@
// src/services/db/connection.ts
// src/services/db/connection.db.ts
import { Pool, PoolConfig } from 'pg';
import { logger } from '../logger';
@@ -31,8 +31,8 @@ const createPool = (): Pool => {
};
// --- ADD DEBUG LOGGING HERE ---
console.log(`[DEBUG] connection.ts: createPool called. poolId=${poolId}`);
console.log(`[DEBUG] connection.ts: typeof Pool is "${typeof Pool}"`);
console.log(`[DEBUG] connection.db.ts: createPool called. poolId=${poolId}`);
console.log(`[DEBUG] connection.db.ts: typeof Pool is "${typeof Pool}"`);
if (typeof Pool !== 'function') {
console.error('[DEBUG] CRITICAL: Pool is NOT a function/class!', Pool);
}
@@ -43,12 +43,12 @@ const createPool = (): Pool => {
newPool.poolId = poolId;
// --- ADDED SUCCESS LOG ---
console.log('[DEBUG] connection.ts: Successfully instantiated Pool.');
console.log('[DEBUG] connection.db.ts: Successfully instantiated Pool.');
// -------------------------
return newPool;
} catch (error) {
console.error('[DEBUG] connection.ts: "new Pool()" threw error:', error);
console.error('[DEBUG] connection.db.ts: "new Pool()" threw error:', error);
throw error;
}
};

View File

@@ -1,4 +1,4 @@
// src/services/db/errors.ts
// src/services/db/errors.db.ts
/**
* Base class for custom database errors to ensure they have a status property.

View File

@@ -1,4 +1,4 @@
// src/services/db/flyer.test.ts
// src/services/db/flyer.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getFlyers,
@@ -13,9 +13,9 @@ import {
updateStoreLogo,
trackFlyerItemInteraction,
getHistoricalPriceDataForItems,
} from './flyer';
} from './flyer.db';
import type { Flyer, FlyerItem } from '../../types';
import { getPool } from './connection';
import { getPool } from './connection.db';
import { Pool } from 'pg';
const mockQuery = vi.fn();
@@ -36,7 +36,7 @@ describe('Flyer DB Service', () => {
beforeEach(() => {
// FIX: Use mockImplementation with a standard function to support 'new Pool()'
vi.mocked(Pool).mockImplementation(function() {
console.log('[DEBUG] flyer.test.ts: Local Pool mock instantiated via "new"');
console.log('[DEBUG] flyer.db.test.ts: Local Pool mock instantiated via "new"');
return {
query: mockQuery,
connect: mockConnect,

View File

@@ -1,6 +1,6 @@
// src/services/db/flyer.ts
import { getPool } from './connection';
import { UniqueConstraintError } from './errors';
// src/services/db/flyer.db.ts
import { getPool } from './connection.db';
import { UniqueConstraintError } from './errors.db';
import { logger } from '../logger.server';
import { geocodeAddress } from '../geocodingService.server';
import { Flyer, Brand, MasterGroceryItem, FlyerItem, Address } from '../../types';

View File

@@ -1,13 +1,13 @@
// src/services/db/gamification.test.ts
// src/services/db/gamification.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getAllAchievements,
getUserAchievements,
awardAchievement,
getLeaderboard,
} from './gamification';
} from './gamification.db';
import type { Achievement, UserAchievement, LeaderboardUser } from '../../types';
import { getPool } from './connection';
import { getPool } from './connection.db';
// Mock the getPool function to return a mocked pool object.
const mockQuery = vi.fn();
@@ -32,7 +32,7 @@ vi.mock('../logger', () => ({
describe('Gamification DB Service', () => {
beforeEach(() => {
vi.clearAllMocks();
console.log('[gamification.test.ts] Mocks cleared');
console.log('[gamification.db.test.ts] Mocks cleared');
});
describe('getAllAchievements', () => {

View File

@@ -1,6 +1,6 @@
// src/services/db/gamification.ts
import { getPool } from './connection';
import { ForeignKeyConstraintError } from './errors';
// src/services/db/gamification.db.ts
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { logger } from '../logger';
import { Achievement, UserAchievement, LeaderboardUser } from '../../types';

View File

@@ -0,0 +1,13 @@
// src/services/db/index.db.ts
export * from './connection.db';
export * from './errors.db';
export * from './user.db';
export * from './flyer.db';
export * from './shopping.db';
export * from './personalization.db';
export * from './recipe.db';
export * from './admin.db';
export * from './notification.db';
export * from './gamification.db';
export * from './budget.db';
export * from './address.db'; // Add the new address service exports

View File

@@ -1,13 +0,0 @@
// src/services/db/index.ts
export * from './connection';
export * from './errors';
export * from './user';
export * from './flyer';
export * from './shopping';
export * from './personalization';
export * from './recipe';
export * from './admin';
export * from './notification';
export * from './gamification';
export * from './budget';
export * from './address'; // Add the new address service exports

View File

@@ -1,4 +1,4 @@
// src/services/db/notification.test.ts
// src/services/db/notification.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getNotificationsForUser,
@@ -6,7 +6,7 @@ import {
createBulkNotifications,
markAllNotificationsAsRead,
markNotificationAsRead,
} from './notification';
} from './notification.db';
import { Pool } from 'pg';
const mockQuery = vi.fn();

View File

@@ -1,6 +1,6 @@
// src/services/db/notification.ts
import { getPool } from './connection';
import { ForeignKeyConstraintError } from './errors';
// src/services/db/notification.db.ts
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { logger } from '../logger';
import { Notification } from '../../types';

View File

@@ -1,4 +1,4 @@
// src/services/db/personalization.test.ts
// src/services/db/personalization.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getWatchedItems,
@@ -16,9 +16,9 @@ import {
getUserAppliances,
setUserAppliances,
getRecipesForUserDiets,
} from './personalization';
} from './personalization.db';
import type { MasterGroceryItem } from '../../types';
import { getPool } from './connection';
import { getPool } from './connection.db';
import { Pool } from 'pg';
const mockQuery = vi.fn();
@@ -39,7 +39,7 @@ describe('Personalization DB Service', () => {
beforeEach(() => {
mockQuery.mockReset();
vi.mocked(Pool).mockImplementation(function() {
console.log('[DEBUG] personalization.test.ts: Local Pool mock instantiated via "new"');
console.log('[DEBUG] personalization.db.test.ts: Local Pool mock instantiated via "new"');
return {
query: mockQuery,
connect: mockConnect,

View File

@@ -1,6 +1,6 @@
// src/services/db/personalization.ts
import { getPool } from './connection';
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors';
// src/services/db/personalization.db.ts
import { getPool } from './connection.db';
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors.db';
import { logger } from '../logger';
import {
MasterGroceryItem,

View File

@@ -1,4 +1,4 @@
// src/services/db/recipe.test.ts
// src/services/db/recipe.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getRecipesBySalePercentage,
@@ -10,7 +10,7 @@ import {
getRecipeComments,
addRecipeComment,
forkRecipe,
} from './recipe';
} from './recipe.db';
import { Pool } from 'pg';
const mockQuery = vi.fn();
@@ -30,7 +30,7 @@ describe('Recipe DB Service', () => {
beforeEach(() => {
// FIX: Use mockImplementation with a standard function to support 'new Pool()'
vi.mocked(Pool).mockImplementation(function() {
console.log('[DEBUG] recipe.test.ts: Local Pool mock instantiated via "new"');
console.log('[DEBUG] recipe.db.test.ts: Local Pool mock instantiated via "new"');
return {
query: mockQuery,
connect: vi.fn().mockResolvedValue({ query: mockQuery, release: vi.fn() }),

View File

@@ -1,6 +1,6 @@
// src/services/db/recipe.ts
import { getPool } from './connection';
import { ForeignKeyConstraintError } from './errors';
// src/services/db/recipe.db.ts
import { getPool } from './connection.db';
import { ForeignKeyConstraintError } from './errors.db';
import { logger } from '../logger';
import { Recipe, FavoriteRecipe, RecipeComment } from '../../types';

Some files were not shown because too many files have changed in this diff Show More