mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
test: expand test coverage across calendar, contacts, and todo apps
Calendar Web (3 new test files, 29 tests): - dateNavigation: getOffsetDate for week/month/agenda views - eventDateHelpers: toDate, getEventStart/End/Times - eventFiltering: calendar visibility, timed/all-day filtering, hour ranges, overflow events, tag filtering Contacts Backend (jest setup + 1 test file, 14 tests): - Add jest config and test scripts - contact.service.spec.ts: findByUserId, findById, create, update, toggleFavorite, toggleArchive, count Contacts Web (1 new test file, 6 tests): - duplicates API: findDuplicates, mergeContacts, dismissDuplicate Todo Backend (2 new test files, 24 tests): - project.service.spec.ts: CRUD, archive, reorder, default project protection - label.service.spec.ts: CRUD with NotFoundException handling Todo Web (2 new test files, 14 tests): - projects API: CRUD, archive, reorder - reminders API: getReminders, createReminder, deleteReminder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8f71ed134d
commit
37a699131c
12 changed files with 2605 additions and 799 deletions
55
apps/calendar/apps/web/src/lib/utils/dateNavigation.test.ts
Normal file
55
apps/calendar/apps/web/src/lib/utils/dateNavigation.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { getOffsetDate } from './dateNavigation';
|
||||
|
||||
describe('getOffsetDate', () => {
|
||||
// Use local time to avoid timezone offset issues with date-fns
|
||||
const baseDate = new Date(2026, 2, 18, 12, 0, 0); // March 18, 2026 12:00
|
||||
|
||||
describe('week view', () => {
|
||||
it('should add 1 week for offset +1', () => {
|
||||
const result = getOffsetDate(baseDate, 'week', 1);
|
||||
expect(result).toEqual(new Date(2026, 2, 25, 12, 0, 0));
|
||||
});
|
||||
|
||||
it('should subtract 1 week for offset -1', () => {
|
||||
const result = getOffsetDate(baseDate, 'week', -1);
|
||||
expect(result).toEqual(new Date(2026, 2, 11, 12, 0, 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('month view', () => {
|
||||
it('should add 1 month for offset +1', () => {
|
||||
const result = getOffsetDate(baseDate, 'month', 1);
|
||||
expect(result).toEqual(new Date(2026, 3, 18, 12, 0, 0));
|
||||
});
|
||||
|
||||
it('should subtract 1 month for offset -1', () => {
|
||||
const result = getOffsetDate(baseDate, 'month', -1);
|
||||
expect(result).toEqual(new Date(2026, 1, 18, 12, 0, 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('agenda view', () => {
|
||||
it('should add 7 days for offset +1', () => {
|
||||
const result = getOffsetDate(baseDate, 'agenda', 1);
|
||||
expect(result).toEqual(new Date(2026, 2, 25, 12, 0, 0));
|
||||
});
|
||||
|
||||
it('should subtract 7 days for offset -1', () => {
|
||||
const result = getOffsetDate(baseDate, 'agenda', -1);
|
||||
expect(result).toEqual(new Date(2026, 2, 11, 12, 0, 0));
|
||||
});
|
||||
|
||||
it('should add 14 days for offset +2', () => {
|
||||
const result = getOffsetDate(baseDate, 'agenda', 2);
|
||||
expect(result).toEqual(new Date(2026, 3, 1, 12, 0, 0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('default (unknown view type)', () => {
|
||||
it('should fall through to week behavior', () => {
|
||||
const result = getOffsetDate(baseDate, 'unknown' as any, 1);
|
||||
expect(result).toEqual(new Date(2026, 2, 25, 12, 0, 0));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { toDate, getEventStart, getEventEnd, getEventTimes } from './eventDateHelpers';
|
||||
|
||||
describe('toDate', () => {
|
||||
it('should parse ISO string to Date', () => {
|
||||
const result = toDate('2026-03-18T10:30:00Z');
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result.toISOString()).toBe('2026-03-18T10:30:00.000Z');
|
||||
});
|
||||
|
||||
it('should return Date object as-is', () => {
|
||||
const date = new Date('2026-03-18T10:30:00Z');
|
||||
const result = toDate(date);
|
||||
expect(result).toBe(date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventStart', () => {
|
||||
it('should extract start time from event with string', () => {
|
||||
const event = { startTime: '2026-03-18T09:00:00Z' };
|
||||
const result = getEventStart(event);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result.toISOString()).toBe('2026-03-18T09:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should handle Date object', () => {
|
||||
const date = new Date('2026-03-18T09:00:00Z');
|
||||
const event = { startTime: date };
|
||||
const result = getEventStart(event);
|
||||
expect(result).toBe(date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventEnd', () => {
|
||||
it('should extract end time from event with string', () => {
|
||||
const event = { endTime: '2026-03-18T10:00:00Z' };
|
||||
const result = getEventEnd(event);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
expect(result.toISOString()).toBe('2026-03-18T10:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should handle Date object', () => {
|
||||
const date = new Date('2026-03-18T10:00:00Z');
|
||||
const event = { endTime: date };
|
||||
const result = getEventEnd(event);
|
||||
expect(result).toBe(date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventTimes', () => {
|
||||
it('should return both start and end as Date objects', () => {
|
||||
const event = {
|
||||
startTime: '2026-03-18T09:00:00Z',
|
||||
endTime: '2026-03-18T10:00:00Z',
|
||||
};
|
||||
const result = getEventTimes(event);
|
||||
expect(result.start).toBeInstanceOf(Date);
|
||||
expect(result.end).toBeInstanceOf(Date);
|
||||
expect(result.start.toISOString()).toBe('2026-03-18T09:00:00.000Z');
|
||||
expect(result.end.toISOString()).toBe('2026-03-18T10:00:00.000Z');
|
||||
});
|
||||
});
|
||||
179
apps/calendar/apps/web/src/lib/utils/eventFiltering.test.ts
Normal file
179
apps/calendar/apps/web/src/lib/utils/eventFiltering.test.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getVisibleCalendarIds,
|
||||
filterByVisibleCalendars,
|
||||
filterTimedEvents,
|
||||
filterAllDayEvents,
|
||||
getEventMinutes,
|
||||
eventOverlapsTimeRange,
|
||||
filterByHourRange,
|
||||
getOverflowEvents,
|
||||
filterByTags,
|
||||
} from './eventFiltering';
|
||||
|
||||
// Mock calendars
|
||||
const calendarA = { id: 'cal-a', name: 'Work', color: '#3B82F6' } as any;
|
||||
const calendarB = { id: 'cal-b', name: 'Personal', color: '#EF4444' } as any;
|
||||
|
||||
// Mock events - use local Date objects to avoid timezone offset issues with getHours()
|
||||
const timedEventA = {
|
||||
id: 'evt-1',
|
||||
calendarId: 'cal-a',
|
||||
startTime: new Date(2026, 2, 18, 9, 0, 0),
|
||||
endTime: new Date(2026, 2, 18, 10, 0, 0),
|
||||
isAllDay: false,
|
||||
tags: [{ id: 'tag-1', name: 'meeting' }],
|
||||
} as any;
|
||||
|
||||
const timedEventB = {
|
||||
id: 'evt-2',
|
||||
calendarId: 'cal-b',
|
||||
startTime: new Date(2026, 2, 18, 14, 0, 0),
|
||||
endTime: new Date(2026, 2, 18, 15, 30, 0),
|
||||
isAllDay: false,
|
||||
tags: [{ id: 'tag-2', name: 'personal' }],
|
||||
} as any;
|
||||
|
||||
const allDayEvent = {
|
||||
id: 'evt-3',
|
||||
calendarId: 'cal-a',
|
||||
startTime: new Date(2026, 2, 18, 0, 0, 0),
|
||||
endTime: new Date(2026, 2, 19, 0, 0, 0),
|
||||
isAllDay: true,
|
||||
tags: [],
|
||||
} as any;
|
||||
|
||||
const earlyEvent = {
|
||||
id: 'evt-4',
|
||||
calendarId: 'cal-a',
|
||||
startTime: new Date(2026, 2, 18, 5, 0, 0),
|
||||
endTime: new Date(2026, 2, 18, 6, 0, 0),
|
||||
isAllDay: false,
|
||||
tags: null,
|
||||
} as any;
|
||||
|
||||
const lateEvent = {
|
||||
id: 'evt-5',
|
||||
calendarId: 'cal-a',
|
||||
startTime: new Date(2026, 2, 18, 22, 0, 0),
|
||||
endTime: new Date(2026, 2, 18, 23, 0, 0),
|
||||
isAllDay: false,
|
||||
tags: [{ id: 'tag-1', name: 'meeting' }],
|
||||
} as any;
|
||||
|
||||
const allEvents = [timedEventA, timedEventB, allDayEvent, earlyEvent, lateEvent];
|
||||
|
||||
describe('getVisibleCalendarIds', () => {
|
||||
it('should return a Set of calendar IDs', () => {
|
||||
const result = getVisibleCalendarIds([calendarA, calendarB]);
|
||||
expect(result).toBeInstanceOf(Set);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.has('cal-a')).toBe(true);
|
||||
expect(result.has('cal-b')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterByVisibleCalendars', () => {
|
||||
it('should filter events to only visible calendars', () => {
|
||||
const result = filterByVisibleCalendars(allEvents, [calendarA]);
|
||||
expect(result.every((e) => e.calendarId === 'cal-a')).toBe(true);
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterTimedEvents', () => {
|
||||
it('should filter out all-day events', () => {
|
||||
const result = filterTimedEvents(allEvents);
|
||||
expect(result.every((e) => !e.isAllDay)).toBe(true);
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterAllDayEvents', () => {
|
||||
it('should keep only all-day events', () => {
|
||||
const result = filterAllDayEvents(allEvents);
|
||||
expect(result.every((e) => e.isAllDay)).toBe(true);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('evt-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventMinutes', () => {
|
||||
it('should convert event times to minutes from midnight', () => {
|
||||
const result = getEventMinutes(timedEventA);
|
||||
// 09:00 UTC = 540 minutes
|
||||
expect(result.start).toBe(9 * 60);
|
||||
// 10:00 UTC = 600 minutes
|
||||
expect(result.end).toBe(10 * 60);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eventOverlapsTimeRange', () => {
|
||||
it('should return true when event overlaps with range', () => {
|
||||
// Event is 09:00-10:00, range is 08:00-12:00
|
||||
const result = eventOverlapsTimeRange(timedEventA, 8 * 60, 12 * 60);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when event is outside range', () => {
|
||||
// Event is 09:00-10:00, range is 11:00-12:00
|
||||
const result = eventOverlapsTimeRange(timedEventA, 11 * 60, 12 * 60);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when event ends exactly at range start', () => {
|
||||
// Event is 09:00-10:00, range starts at 10:00
|
||||
const result = eventOverlapsTimeRange(timedEventA, 10 * 60, 12 * 60);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterByHourRange', () => {
|
||||
it('should filter events within the hour range', () => {
|
||||
const timedEvents = filterTimedEvents(allEvents);
|
||||
// Range 8:00-18:00 should include timedEventA (9-10) and timedEventB (14-15:30)
|
||||
const result = filterByHourRange(timedEvents, 8, 18);
|
||||
expect(result.some((e) => e.id === 'evt-1')).toBe(true);
|
||||
expect(result.some((e) => e.id === 'evt-2')).toBe(true);
|
||||
// earlyEvent (5-6) and lateEvent (22-23) should be excluded
|
||||
expect(result.some((e) => e.id === 'evt-4')).toBe(false);
|
||||
expect(result.some((e) => e.id === 'evt-5')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverflowEvents', () => {
|
||||
it('should return events before and after visible range', () => {
|
||||
const timedEvents = filterTimedEvents(allEvents);
|
||||
const result = getOverflowEvents(timedEvents, 8, 18);
|
||||
|
||||
// earlyEvent (5-6) ends before 8:00
|
||||
expect(result.before.some((e) => e.id === 'evt-4')).toBe(true);
|
||||
// lateEvent (22-23) starts after 18:00
|
||||
expect(result.after.some((e) => e.id === 'evt-5')).toBe(true);
|
||||
// timedEventA (9-10) is within range, should not appear
|
||||
expect(result.before.some((e) => e.id === 'evt-1')).toBe(false);
|
||||
expect(result.after.some((e) => e.id === 'evt-1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterByTags', () => {
|
||||
it('should return all events when no tags are selected', () => {
|
||||
const result = filterByTags(allEvents, []);
|
||||
expect(result).toHaveLength(allEvents.length);
|
||||
});
|
||||
|
||||
it('should filter events by selected tag IDs', () => {
|
||||
const result = filterByTags(allEvents, ['tag-1']);
|
||||
// timedEventA and lateEvent have tag-1
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.some((e) => e.id === 'evt-1')).toBe(true);
|
||||
expect(result.some((e) => e.id === 'evt-5')).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude events with no tags when filtering', () => {
|
||||
// allDayEvent has empty tags, earlyEvent has null tags
|
||||
const result = filterByTags(allEvents, ['tag-2']);
|
||||
expect(result.some((e) => e.id === 'evt-3')).toBe(false);
|
||||
expect(result.some((e) => e.id === 'evt-4')).toBe(false);
|
||||
});
|
||||
});
|
||||
16
apps/contacts/apps/backend/jest.config.js
Normal file
16
apps/contacts/apps/backend/jest.config.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||
rootDir: 'src',
|
||||
testRegex: '.*\\.spec\\.ts$',
|
||||
transform: {
|
||||
'^.+\\.(t|j)s$': 'ts-jest',
|
||||
},
|
||||
collectCoverageFrom: ['**/*.(t|j)s'],
|
||||
coverageDirectory: '../coverage',
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'^@contacts/shared$': '<rootDir>/../../packages/shared/src',
|
||||
'^@manacore/shared-nestjs-auth$': '<rootDir>/../../../../../packages/shared-nestjs-auth/src',
|
||||
},
|
||||
};
|
||||
|
|
@ -15,7 +15,10 @@
|
|||
"migration:run": "tsx src/db/migrate.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:seed": "tsx src/db/seed.ts"
|
||||
"db:seed": "tsx src/db/seed.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/shared-nestjs-auth": "workspace:*",
|
||||
|
|
@ -44,7 +47,9 @@
|
|||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@nestjs/testing": "^11.1.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^22.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
||||
|
|
@ -52,8 +57,10 @@
|
|||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"jest": "^30.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { ContactService } from '../contact.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
describe('ContactService', () => {
|
||||
let service: ContactService;
|
||||
let mockDb: any;
|
||||
|
||||
const mockContact = {
|
||||
id: 'contact-1',
|
||||
userId: 'user-1',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
displayName: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '+1234567890',
|
||||
mobile: null,
|
||||
nickname: null,
|
||||
street: null,
|
||||
city: null,
|
||||
postalCode: null,
|
||||
country: null,
|
||||
company: 'Acme Inc',
|
||||
jobTitle: null,
|
||||
department: null,
|
||||
website: null,
|
||||
birthday: null,
|
||||
notes: null,
|
||||
photoUrl: null,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
organizationId: null,
|
||||
teamId: null,
|
||||
visibility: 'private',
|
||||
sharedWith: null,
|
||||
createdAt: new Date('2025-01-01'),
|
||||
updatedAt: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
offset: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ContactService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ContactService>(ContactService);
|
||||
});
|
||||
|
||||
describe('findByUserId', () => {
|
||||
it('should return contacts without search filters', async () => {
|
||||
mockDb.offset.mockResolvedValue([mockContact]);
|
||||
|
||||
const result = await service.findByUserId('user-1');
|
||||
|
||||
expect(result).toEqual([mockContact]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return contacts with search filter using relevance scoring', async () => {
|
||||
const searchResult = [{ contact: mockContact, relevance: 100 }];
|
||||
mockDb.offset.mockResolvedValue(searchResult);
|
||||
|
||||
const result = await service.findByUserId('user-1', { search: 'John' });
|
||||
|
||||
expect(result).toEqual([mockContact]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a contact when found', async () => {
|
||||
mockDb.where.mockResolvedValue([mockContact]);
|
||||
|
||||
const result = await service.findById('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual(mockContact);
|
||||
});
|
||||
|
||||
it('should return null when contact is not found', async () => {
|
||||
mockDb.where.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findById('nonexistent', 'user-1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should insert and return a new contact', async () => {
|
||||
mockDb.returning.mockResolvedValue([mockContact]);
|
||||
|
||||
const newContact = {
|
||||
userId: 'user-1',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john@example.com',
|
||||
};
|
||||
|
||||
const result = await service.create(newContact as any);
|
||||
|
||||
expect(result).toEqual(mockContact);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalledWith(newContact);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update and return the contact', async () => {
|
||||
const updatedContact = { ...mockContact, firstName: 'Jane' };
|
||||
mockDb.returning.mockResolvedValue([updatedContact]);
|
||||
|
||||
const result = await service.update('contact-1', 'user-1', { firstName: 'Jane' });
|
||||
|
||||
expect(result).toEqual(updatedContact);
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when contact is not found', async () => {
|
||||
mockDb.returning.mockResolvedValue([]);
|
||||
|
||||
await expect(service.update('nonexistent', 'user-1', { firstName: 'Jane' })).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFavorite', () => {
|
||||
it('should toggle isFavorite from false to true', async () => {
|
||||
const toggledContact = { ...mockContact, isFavorite: true };
|
||||
|
||||
// findById call
|
||||
mockDb.where.mockResolvedValueOnce([mockContact]);
|
||||
// update call
|
||||
mockDb.returning.mockResolvedValue([toggledContact]);
|
||||
|
||||
const result = await service.toggleFavorite('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual(toggledContact);
|
||||
expect(result.isFavorite).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when contact is not found', async () => {
|
||||
mockDb.where.mockResolvedValue([]);
|
||||
|
||||
await expect(service.toggleFavorite('nonexistent', 'user-1')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleArchive', () => {
|
||||
it('should toggle isArchived from false to true', async () => {
|
||||
const archivedContact = { ...mockContact, isArchived: true };
|
||||
|
||||
// findById call
|
||||
mockDb.where.mockResolvedValueOnce([mockContact]);
|
||||
// update call
|
||||
mockDb.returning.mockResolvedValue([archivedContact]);
|
||||
|
||||
const result = await service.toggleArchive('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual(archivedContact);
|
||||
expect(result.isArchived).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when contact is not found', async () => {
|
||||
mockDb.where.mockResolvedValue([]);
|
||||
|
||||
await expect(service.toggleArchive('nonexistent', 'user-1')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('should return the count of contacts', async () => {
|
||||
mockDb.where.mockResolvedValue([{ count: 42 }]);
|
||||
|
||||
const result = await service.count('user-1');
|
||||
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it('should return 0 when no contacts exist', async () => {
|
||||
mockDb.where.mockResolvedValue([{ count: 0 }]);
|
||||
|
||||
const result = await service.count('user-1');
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a contact successfully', async () => {
|
||||
// delete call
|
||||
mockDb.where.mockResolvedValueOnce(undefined);
|
||||
// findById check (contact no longer exists)
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.delete('contact-1', 'user-1')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
139
apps/contacts/apps/web/src/lib/api/duplicates.test.ts
Normal file
139
apps/contacts/apps/web/src/lib/api/duplicates.test.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { DuplicateGroup, MergeResult } from './duplicates';
|
||||
|
||||
vi.mock('$lib/stores/auth.svelte', () => ({
|
||||
authStore: {
|
||||
getAccessToken: vi.fn().mockResolvedValue('mock-token'),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./config', () => ({
|
||||
API_BASE: 'http://localhost:3015/api/v1',
|
||||
}));
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
// Import after mocks are set up
|
||||
const { duplicatesApi } = await import('./duplicates');
|
||||
|
||||
describe('duplicatesApi', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findDuplicates', () => {
|
||||
it('should call GET /duplicates and return duplicates', async () => {
|
||||
const mockResponse: { duplicates: DuplicateGroup[]; total: number } = {
|
||||
duplicates: [
|
||||
{
|
||||
id: 'group-1',
|
||||
contacts: [
|
||||
{ id: 'c1', firstName: 'John', email: 'john@example.com' } as any,
|
||||
{ id: 'c2', firstName: 'John', email: 'john@example.com' } as any,
|
||||
],
|
||||
matchType: 'email',
|
||||
matchValue: 'john@example.com',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue(mockResponse),
|
||||
});
|
||||
|
||||
const result = await duplicatesApi.findDuplicates();
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3015/api/v1/duplicates', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-token',
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should throw an error when the request fails', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
json: vi.fn().mockResolvedValue({ message: 'Unauthorized' }),
|
||||
});
|
||||
|
||||
await expect(duplicatesApi.findDuplicates()).rejects.toThrow('Unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeContacts', () => {
|
||||
it('should call POST /duplicates/merge with correct body', async () => {
|
||||
const mockResult: MergeResult = {
|
||||
mergedContact: {
|
||||
id: 'c1',
|
||||
firstName: 'John',
|
||||
email: 'john@example.com',
|
||||
} as any,
|
||||
deletedIds: ['c2', 'c3'],
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue(mockResult),
|
||||
});
|
||||
|
||||
const result = await duplicatesApi.mergeContacts('c1', ['c2', 'c3']);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3015/api/v1/duplicates/merge', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ primaryId: 'c1', mergeIds: ['c2', 'c3'] }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-token',
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
|
||||
it('should throw an error when merge fails', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
json: vi.fn().mockResolvedValue({ message: 'Merge conflict' }),
|
||||
});
|
||||
|
||||
await expect(duplicatesApi.mergeContacts('c1', ['c2'])).rejects.toThrow('Merge conflict');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissDuplicate', () => {
|
||||
it('should call DELETE /duplicates/:groupId/dismiss', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
await duplicatesApi.dismissDuplicate('group-1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:3015/api/v1/duplicates/group-1/dismiss',
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer mock-token',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when dismiss fails', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
json: vi.fn().mockResolvedValue({ message: 'Group not found' }),
|
||||
});
|
||||
|
||||
await expect(duplicatesApi.dismissDuplicate('nonexistent')).rejects.toThrow(
|
||||
'Group not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
189
apps/todo/apps/backend/src/label/__tests__/label.service.spec.ts
Normal file
189
apps/todo/apps/backend/src/label/__tests__/label.service.spec.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { LabelService } from '../label.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
const mockDb = {
|
||||
query: {
|
||||
labels: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
},
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn(),
|
||||
};
|
||||
|
||||
describe('LabelService', () => {
|
||||
let service: LabelService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LabelService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LabelService>(LabelService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all labels for a user', async () => {
|
||||
const userId = 'user-123';
|
||||
const mockLabels = [
|
||||
{ id: 'label-1', name: 'Important', color: '#ff0000', userId },
|
||||
{ id: 'label-2', name: 'Work', color: '#0000ff', userId },
|
||||
];
|
||||
|
||||
mockDb.query.labels.findMany.mockResolvedValue(mockLabels);
|
||||
|
||||
const result = await service.findAll(userId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockDb.query.labels.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when no labels', async () => {
|
||||
mockDb.query.labels.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findAll('user-123');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a label when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const labelId = 'label-1';
|
||||
const mockLabel = { id: labelId, name: 'Important', color: '#ff0000', userId };
|
||||
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(mockLabel);
|
||||
|
||||
const result = await service.findById(labelId, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(labelId);
|
||||
});
|
||||
|
||||
it('should return null when label not found', async () => {
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.findById('non-existent', 'user-123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdOrThrow', () => {
|
||||
it('should return a label when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const labelId = 'label-1';
|
||||
const mockLabel = { id: labelId, name: 'Important', color: '#ff0000', userId };
|
||||
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(mockLabel);
|
||||
|
||||
const result = await service.findByIdOrThrow(labelId, userId);
|
||||
|
||||
expect(result.id).toBe(labelId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when label not found', async () => {
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.findByIdOrThrow('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a label', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'Urgent', color: '#ff0000' };
|
||||
const createdLabel = { id: 'label-new', ...dto, userId };
|
||||
|
||||
mockDb.returning.mockResolvedValue([createdLabel]);
|
||||
|
||||
const result = await service.create(userId, dto);
|
||||
|
||||
expect(result.name).toBe('Urgent');
|
||||
expect(result.color).toBe('#ff0000');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a label with default color when not provided', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'Simple' };
|
||||
const createdLabel = { id: 'label-new', name: 'Simple', color: '#6B7280', userId };
|
||||
|
||||
mockDb.returning.mockResolvedValue([createdLabel]);
|
||||
|
||||
const result = await service.create(userId, dto);
|
||||
|
||||
expect(result.name).toBe('Simple');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a label', async () => {
|
||||
const userId = 'user-123';
|
||||
const labelId = 'label-1';
|
||||
const dto = { name: 'Updated Label', color: '#00ff00' };
|
||||
const existingLabel = { id: labelId, name: 'Original', color: '#ff0000', userId };
|
||||
const updatedLabel = { id: labelId, ...dto, userId };
|
||||
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(existingLabel);
|
||||
mockDb.returning.mockResolvedValue([updatedLabel]);
|
||||
|
||||
const result = await service.update(labelId, userId, dto);
|
||||
|
||||
expect(result.name).toBe('Updated Label');
|
||||
expect(result.color).toBe('#00ff00');
|
||||
});
|
||||
|
||||
it('should throw when label does not exist', async () => {
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.update('non-existent', 'user-123', { name: 'Test' })).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a label', async () => {
|
||||
const userId = 'user-123';
|
||||
const labelId = 'label-1';
|
||||
const existingLabel = { id: labelId, name: 'Important', userId };
|
||||
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(existingLabel);
|
||||
|
||||
await service.delete(labelId, userId);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when label does not exist', async () => {
|
||||
mockDb.query.labels.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.delete('non-existent', 'user-123')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { ProjectService } from '../project.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
const mockDb = {
|
||||
query: {
|
||||
projects: {
|
||||
findMany: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
},
|
||||
},
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn(),
|
||||
};
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let service: ProjectService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ProjectService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ProjectService>(ProjectService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return all projects for a user', async () => {
|
||||
const userId = 'user-123';
|
||||
const mockProjects = [
|
||||
{ id: 'proj-1', name: 'Work', userId, order: 0 },
|
||||
{ id: 'proj-2', name: 'Personal', userId, order: 1 },
|
||||
];
|
||||
|
||||
mockDb.query.projects.findMany.mockResolvedValue(mockProjects);
|
||||
|
||||
const result = await service.findAll(userId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(mockDb.query.projects.findMany).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when no projects', async () => {
|
||||
mockDb.query.projects.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findAll('user-123');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a project when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectId = 'proj-1';
|
||||
const mockProject = { id: projectId, name: 'Work', userId };
|
||||
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(mockProject);
|
||||
|
||||
const result = await service.findById(projectId, userId);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(projectId);
|
||||
});
|
||||
|
||||
it('should return null when project not found', async () => {
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.findById('non-existent', 'user-123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdOrThrow', () => {
|
||||
it('should return a project when found', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectId = 'proj-1';
|
||||
const mockProject = { id: projectId, name: 'Work', userId };
|
||||
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(mockProject);
|
||||
|
||||
const result = await service.findByIdOrThrow(projectId, userId);
|
||||
|
||||
expect(result.id).toBe(projectId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when project not found', async () => {
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.findByIdOrThrow('non-existent', 'user-123')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a project with correct order', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'New Project', color: '#ff0000', icon: 'star' };
|
||||
const existingProjects = [
|
||||
{ id: 'proj-1', name: 'Work', userId, order: 0 },
|
||||
{ id: 'proj-2', name: 'Personal', userId, order: 1 },
|
||||
];
|
||||
const createdProject = { id: 'proj-new', ...dto, userId, order: 2 };
|
||||
|
||||
mockDb.query.projects.findMany.mockResolvedValue(existingProjects);
|
||||
mockDb.returning.mockResolvedValue([createdProject]);
|
||||
|
||||
const result = await service.create(userId, dto);
|
||||
|
||||
expect(result.order).toBe(2);
|
||||
expect(result.name).toBe('New Project');
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set order to 0 when no existing projects', async () => {
|
||||
const userId = 'user-123';
|
||||
const dto = { name: 'First Project' };
|
||||
const createdProject = {
|
||||
id: 'proj-first',
|
||||
name: 'First Project',
|
||||
userId,
|
||||
order: 0,
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
mockDb.query.projects.findMany.mockResolvedValue([]);
|
||||
mockDb.returning.mockResolvedValue([createdProject]);
|
||||
|
||||
const result = await service.create(userId, dto);
|
||||
|
||||
expect(result.order).toBe(0);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a project', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectId = 'proj-1';
|
||||
const dto = { name: 'Updated Name' };
|
||||
const existingProject = { id: projectId, name: 'Original', userId };
|
||||
const updatedProject = { id: projectId, name: 'Updated Name', userId };
|
||||
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(existingProject);
|
||||
mockDb.returning.mockResolvedValue([updatedProject]);
|
||||
|
||||
const result = await service.update(projectId, userId, dto);
|
||||
|
||||
expect(result.name).toBe('Updated Name');
|
||||
});
|
||||
|
||||
it('should throw when project does not exist', async () => {
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.update('non-existent', 'user-123', { name: 'Test' })).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a project', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectId = 'proj-1';
|
||||
const existingProject = { id: projectId, userId, isDefault: false };
|
||||
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(existingProject);
|
||||
|
||||
await service.delete(projectId, userId);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when project does not exist', async () => {
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.delete('non-existent', 'user-123')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw when trying to delete the default project', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectId = 'proj-default';
|
||||
const defaultProject = { id: projectId, userId, isDefault: true };
|
||||
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(defaultProject);
|
||||
|
||||
await expect(service.delete(projectId, userId)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('archive', () => {
|
||||
it('should archive a project', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectId = 'proj-1';
|
||||
const existingProject = { id: projectId, userId, isArchived: false };
|
||||
const archivedProject = { ...existingProject, isArchived: true };
|
||||
|
||||
mockDb.query.projects.findFirst.mockResolvedValue(existingProject);
|
||||
mockDb.returning.mockResolvedValue([archivedProject]);
|
||||
|
||||
const result = await service.archive(projectId, userId);
|
||||
|
||||
expect(result.isArchived).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorder', () => {
|
||||
it('should update order for each project and return all', async () => {
|
||||
const userId = 'user-123';
|
||||
const projectIds = ['proj-2', 'proj-1', 'proj-3'];
|
||||
const reorderedProjects = [
|
||||
{ id: 'proj-2', order: 0 },
|
||||
{ id: 'proj-1', order: 1 },
|
||||
{ id: 'proj-3', order: 2 },
|
||||
];
|
||||
|
||||
// Mock for the final findAll call
|
||||
mockDb.query.projects.findMany.mockResolvedValue(reorderedProjects);
|
||||
|
||||
const result = await service.reorder(userId, projectIds);
|
||||
|
||||
expect(mockDb.update).toHaveBeenCalledTimes(3);
|
||||
expect(result).toEqual(reorderedProjects);
|
||||
});
|
||||
});
|
||||
});
|
||||
134
apps/todo/apps/web/src/lib/api/projects.test.ts
Normal file
134
apps/todo/apps/web/src/lib/api/projects.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the client module
|
||||
vi.mock('./client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
getProjects,
|
||||
getProject,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
archiveProject,
|
||||
reorderProjects,
|
||||
} from './projects';
|
||||
import { apiClient } from './client';
|
||||
|
||||
const mockClient = vi.mocked(apiClient);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getProjects', () => {
|
||||
it('should fetch all projects', async () => {
|
||||
const projects = [
|
||||
{ id: 'p1', name: 'Work' },
|
||||
{ id: 'p2', name: 'Personal' },
|
||||
];
|
||||
mockClient.get.mockResolvedValue({ projects });
|
||||
|
||||
const result = await getProjects();
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/projects');
|
||||
expect(result).toEqual(projects);
|
||||
});
|
||||
|
||||
it('should return empty array when no projects', async () => {
|
||||
mockClient.get.mockResolvedValue({ projects: [] });
|
||||
|
||||
const result = await getProjects();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProject', () => {
|
||||
it('should fetch a single project by id', async () => {
|
||||
const project = { id: 'p1', name: 'Work' };
|
||||
mockClient.get.mockResolvedValue({ project });
|
||||
|
||||
const result = await getProject('p1');
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/projects/p1');
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProject', () => {
|
||||
it('should POST a new project', async () => {
|
||||
const data = { name: 'New Project', color: '#ff0000', icon: 'star' };
|
||||
const project = { id: 'p-new', ...data };
|
||||
mockClient.post.mockResolvedValue({ project });
|
||||
|
||||
const result = await createProject(data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/projects', data);
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
|
||||
it('should create project with only name', async () => {
|
||||
const data = { name: 'Minimal Project' };
|
||||
const project = { id: 'p-min', name: 'Minimal Project' };
|
||||
mockClient.post.mockResolvedValue({ project });
|
||||
|
||||
const result = await createProject(data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/projects', data);
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProject', () => {
|
||||
it('should PUT updated project', async () => {
|
||||
const data = { name: 'Updated Name', color: '#00ff00' };
|
||||
const project = { id: 'p1', ...data };
|
||||
mockClient.put.mockResolvedValue({ project });
|
||||
|
||||
const result = await updateProject('p1', data);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/projects/p1', data);
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteProject', () => {
|
||||
it('should DELETE project', async () => {
|
||||
mockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
await deleteProject('p1');
|
||||
|
||||
expect(mockClient.delete).toHaveBeenCalledWith('/api/v1/projects/p1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('archiveProject', () => {
|
||||
it('should POST to archive endpoint', async () => {
|
||||
const project = { id: 'p1', name: 'Work', isArchived: true };
|
||||
mockClient.post.mockResolvedValue({ project });
|
||||
|
||||
const result = await archiveProject('p1');
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/projects/p1/archive');
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderProjects', () => {
|
||||
it('should PUT reorder with project IDs', async () => {
|
||||
mockClient.put.mockResolvedValue(undefined);
|
||||
|
||||
await reorderProjects(['p1', 'p2', 'p3']);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/projects/reorder', {
|
||||
projectIds: ['p1', 'p2', 'p3'],
|
||||
});
|
||||
});
|
||||
});
|
||||
77
apps/todo/apps/web/src/lib/api/reminders.test.ts
Normal file
77
apps/todo/apps/web/src/lib/api/reminders.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the client module
|
||||
vi.mock('./client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { getReminders, createReminder, deleteReminder } from './reminders';
|
||||
import { apiClient } from './client';
|
||||
|
||||
const mockClient = vi.mocked(apiClient);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getReminders', () => {
|
||||
it('should fetch reminders for a task', async () => {
|
||||
const reminders = [
|
||||
{ id: 'r1', taskId: 't1', minutesBefore: 15, type: 'push' },
|
||||
{ id: 'r2', taskId: 't1', minutesBefore: 60, type: 'email' },
|
||||
];
|
||||
mockClient.get.mockResolvedValue({ reminders });
|
||||
|
||||
const result = await getReminders('t1');
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks/t1/reminders');
|
||||
expect(result).toEqual(reminders);
|
||||
});
|
||||
|
||||
it('should return empty array when no reminders', async () => {
|
||||
mockClient.get.mockResolvedValue({ reminders: [] });
|
||||
|
||||
const result = await getReminders('t1');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReminder', () => {
|
||||
it('should POST a new reminder', async () => {
|
||||
const data = { minutesBefore: 30, type: 'push' as const };
|
||||
const reminder = { id: 'r-new', taskId: 't1', ...data };
|
||||
mockClient.post.mockResolvedValue({ reminder });
|
||||
|
||||
const result = await createReminder('t1', data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/reminders', data);
|
||||
expect(result).toEqual(reminder);
|
||||
});
|
||||
|
||||
it('should create reminder with only minutesBefore', async () => {
|
||||
const data = { minutesBefore: 10 };
|
||||
const reminder = { id: 'r-new', taskId: 't1', minutesBefore: 10 };
|
||||
mockClient.post.mockResolvedValue({ reminder });
|
||||
|
||||
const result = await createReminder('t1', data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/reminders', data);
|
||||
expect(result).toEqual(reminder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteReminder', () => {
|
||||
it('should DELETE reminder by id', async () => {
|
||||
mockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
await deleteReminder('r1');
|
||||
|
||||
expect(mockClient.delete).toHaveBeenCalledWith('/api/v1/reminders/r1');
|
||||
});
|
||||
});
|
||||
2071
pnpm-lock.yaml
generated
2071
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue