Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 45s
225 lines
8.2 KiB
TypeScript
225 lines
8.2 KiB
TypeScript
// src/services/db/reaction.db.test.ts
|
|
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
|
import type { Pool, PoolClient } from 'pg';
|
|
import { ReactionRepository } from './reaction.db';
|
|
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
|
import { withTransaction } from './connection.db';
|
|
import { ForeignKeyConstraintError } from './errors.db';
|
|
import type { UserReaction } from '../../types';
|
|
|
|
// Un-mock the module we are testing
|
|
vi.unmock('./reaction.db');
|
|
|
|
// Mock dependencies
|
|
vi.mock('../logger.server', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
import { logger as mockLogger } from '../logger.server';
|
|
|
|
vi.mock('./connection.db', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('./connection.db')>();
|
|
return { ...actual, withTransaction: vi.fn() };
|
|
});
|
|
|
|
describe('Reaction DB Service', () => {
|
|
let reactionRepo: ReactionRepository;
|
|
const mockDb = {
|
|
query: vi.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
reactionRepo = new ReactionRepository(mockDb);
|
|
});
|
|
|
|
describe('getReactions', () => {
|
|
it('should build a query with no filters', async () => {
|
|
mockDb.query.mockResolvedValue({ rows: [] });
|
|
await reactionRepo.getReactions({}, mockLogger);
|
|
expect(mockDb.query).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.user_reactions WHERE 1=1 ORDER BY created_at DESC',
|
|
[],
|
|
);
|
|
});
|
|
|
|
it('should build a query with a userId filter', async () => {
|
|
mockDb.query.mockResolvedValue({ rows: [] });
|
|
await reactionRepo.getReactions({ userId: 'user-1' }, mockLogger);
|
|
expect(mockDb.query).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.user_reactions WHERE 1=1 AND user_id = $1 ORDER BY created_at DESC',
|
|
['user-1'],
|
|
);
|
|
});
|
|
|
|
it('should build a query with all filters', async () => {
|
|
mockDb.query.mockResolvedValue({ rows: [] });
|
|
await reactionRepo.getReactions(
|
|
{ userId: 'user-1', entityType: 'recipe', entityId: '123' },
|
|
mockLogger,
|
|
);
|
|
expect(mockDb.query).toHaveBeenCalledWith(
|
|
'SELECT * FROM public.user_reactions WHERE 1=1 AND user_id = $1 AND entity_type = $2 AND entity_id = $3 ORDER BY created_at DESC',
|
|
['user-1', 'recipe', '123'],
|
|
);
|
|
});
|
|
|
|
it('should return an array of reactions on success', async () => {
|
|
const mockReactions: UserReaction[] = [
|
|
{
|
|
reaction_id: 1,
|
|
user_id: 'user-1',
|
|
entity_type: 'recipe',
|
|
entity_id: '123',
|
|
reaction_type: 'like',
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
},
|
|
];
|
|
mockDb.query.mockResolvedValue({ rows: mockReactions });
|
|
const result = await reactionRepo.getReactions({}, mockLogger);
|
|
expect(result).toEqual(mockReactions);
|
|
});
|
|
|
|
it('should throw an error if the database query fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
mockDb.query.mockRejectedValue(dbError);
|
|
await expect(reactionRepo.getReactions({}, mockLogger)).rejects.toThrow(
|
|
'Failed to retrieve user reactions.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, filters: {} },
|
|
'Database error in getReactions',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('toggleReaction', () => {
|
|
const reactionData = {
|
|
user_id: 'user-1',
|
|
entity_type: 'recipe',
|
|
entity_id: '123',
|
|
reaction_type: 'like',
|
|
};
|
|
|
|
it('should remove an existing reaction and return null', async () => {
|
|
const mockClient = { query: vi.fn() };
|
|
// Mock DELETE returning 1 row, indicating a reaction was deleted
|
|
(mockClient.query as Mock).mockResolvedValueOnce({ rowCount: 1 });
|
|
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
const result = await reactionRepo.toggleReaction(reactionData, mockLogger);
|
|
|
|
expect(result).toBeNull();
|
|
expect(mockClient.query).toHaveBeenCalledWith(
|
|
'DELETE FROM public.user_reactions WHERE user_id = $1 AND entity_type = $2 AND entity_id = $3 AND reaction_type = $4',
|
|
['user-1', 'recipe', '123', 'like'],
|
|
);
|
|
// Ensure INSERT was not called
|
|
expect(mockClient.query).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should add a new reaction and return it if it does not exist', async () => {
|
|
const mockClient = { query: vi.fn() };
|
|
const mockCreatedReaction: UserReaction = {
|
|
reaction_id: 1,
|
|
...reactionData,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
|
|
// Mock DELETE returning 0 rows, then mock INSERT returning the new reaction
|
|
(mockClient.query as Mock)
|
|
.mockResolvedValueOnce({ rowCount: 0 }) // DELETE
|
|
.mockResolvedValueOnce({ rows: [mockCreatedReaction] }); // INSERT
|
|
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
return callback(mockClient as unknown as PoolClient);
|
|
});
|
|
|
|
const result = await reactionRepo.toggleReaction(reactionData, mockLogger);
|
|
|
|
expect(result).toEqual(mockCreatedReaction);
|
|
expect(mockClient.query).toHaveBeenCalledTimes(2);
|
|
expect(mockClient.query).toHaveBeenCalledWith(
|
|
'INSERT INTO public.user_reactions (user_id, entity_type, entity_id, reaction_type) VALUES ($1, $2, $3, $4) RETURNING *',
|
|
['user-1', 'recipe', '123', 'like'],
|
|
);
|
|
});
|
|
|
|
it('should throw ForeignKeyConstraintError if user or entity does not exist', async () => {
|
|
const dbError = new Error('violates foreign key constraint');
|
|
(dbError as Error & { code: string }).code = '23503';
|
|
|
|
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
|
const mockClient = { query: vi.fn().mockRejectedValue(dbError) };
|
|
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(dbError);
|
|
throw dbError;
|
|
});
|
|
|
|
await expect(reactionRepo.toggleReaction(reactionData, mockLogger)).rejects.toThrow(
|
|
ForeignKeyConstraintError,
|
|
);
|
|
await expect(reactionRepo.toggleReaction(reactionData, mockLogger)).rejects.toThrow(
|
|
'The specified user or entity does not exist.',
|
|
);
|
|
});
|
|
|
|
it('should throw a generic error if the transaction fails', async () => {
|
|
const dbError = new Error('Transaction failed');
|
|
vi.mocked(withTransaction).mockRejectedValue(dbError);
|
|
|
|
await expect(reactionRepo.toggleReaction(reactionData, mockLogger)).rejects.toThrow(
|
|
'Failed to toggle user reaction.',
|
|
);
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, reactionData },
|
|
'Database error in toggleReaction',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getReactionSummary', () => {
|
|
it('should return a summary of reactions for an entity', async () => {
|
|
const mockSummary = [
|
|
{ reaction_type: 'like', count: 5 },
|
|
{ reaction_type: 'heart', count: 2 },
|
|
];
|
|
// This method uses getPool() directly, so we mock the main instance
|
|
mockPoolInstance.query.mockResolvedValue({ rows: mockSummary });
|
|
|
|
const result = await reactionRepo.getReactionSummary('recipe', '123', mockLogger);
|
|
|
|
expect(result).toEqual(mockSummary);
|
|
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
|
expect.stringContaining('GROUP BY reaction_type'),
|
|
['recipe', '123'],
|
|
);
|
|
});
|
|
|
|
it('should return an empty array if there are no reactions', async () => {
|
|
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
|
const result = await reactionRepo.getReactionSummary('recipe', '456', mockLogger);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should throw an error if the database query fails', async () => {
|
|
const dbError = new Error('DB Error');
|
|
mockPoolInstance.query.mockRejectedValue(dbError);
|
|
await expect(
|
|
reactionRepo.getReactionSummary('recipe', '123', mockLogger),
|
|
).rejects.toThrow('Failed to retrieve reaction summary.');
|
|
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError, entityType: 'recipe', entityId: '123' },
|
|
'Database error in getReactionSummary',
|
|
);
|
|
});
|
|
});
|
|
}); |