test(calendar,contacts,todo): add controller unit tests for all 3 apps

- Calendar: 3 controller specs (calendar: 6, event: 7, event-tag: 6) → 151 total
- Contacts: 2 controller specs (contact: 9, tag: 8) → 72 total
- Todo: 2 controller specs (task: 14, project: 6) → 127 total
Uses direct instantiation pattern to avoid NestJS DI complexity in unit tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-19 12:09:01 +01:00
parent 66e2cdc989
commit c15bd05305
7 changed files with 702 additions and 0 deletions

View file

@ -0,0 +1,91 @@
import { CalendarController } from './calendar.controller';
import { createMockCalendar, TEST_USER_ID } from '../__tests__/utils/mock-factories';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
describe('CalendarController', () => {
let controller: CalendarController;
let service: any;
beforeEach(() => {
service = {
findAll: jest.fn(),
findByIdOrThrow: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
getOrCreateDefaultCalendar: jest.fn(),
};
controller = new CalendarController(service);
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return user calendars', async () => {
const calendars = [createMockCalendar(), createMockCalendar()];
service.findAll.mockResolvedValue(calendars);
const result = await controller.findAll(mockUser as any);
expect(result).toEqual({ calendars });
expect(service.findAll).toHaveBeenCalledWith(TEST_USER_ID);
});
it('should lazy-create default calendar when none exist', async () => {
const defaultCal = createMockCalendar({ isDefault: true });
service.findAll.mockResolvedValue([]);
service.getOrCreateDefaultCalendar.mockResolvedValue(defaultCal);
const result = await controller.findAll(mockUser as any);
expect(result).toEqual({ calendars: [defaultCal] });
expect(service.getOrCreateDefaultCalendar).toHaveBeenCalledWith(TEST_USER_ID);
});
});
describe('findOne', () => {
it('should return calendar by id', async () => {
const calendar = createMockCalendar();
service.findByIdOrThrow.mockResolvedValue(calendar);
const result = await controller.findOne(mockUser as any, calendar.id);
expect(result).toEqual({ calendar });
});
});
describe('create', () => {
it('should create and return calendar', async () => {
const calendar = createMockCalendar({ name: 'New Cal' });
service.create.mockResolvedValue(calendar);
const result = await controller.create(mockUser as any, { name: 'New Cal' } as any);
expect(result).toEqual({ calendar });
});
});
describe('update', () => {
it('should update and return calendar', async () => {
const calendar = createMockCalendar({ name: 'Updated' });
service.update.mockResolvedValue(calendar);
const result = await controller.update(mockUser as any, calendar.id, {
name: 'Updated',
} as any);
expect(result).toEqual({ calendar });
});
});
describe('delete', () => {
it('should delete and return success', async () => {
service.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'cal-id');
expect(result).toEqual({ success: true });
});
});
});

View file

@ -0,0 +1,87 @@
import { NotFoundException } from '@nestjs/common';
import { EventTagController } from './event-tag.controller';
import { TEST_USER_ID } from '../__tests__/utils/mock-factories';
import { v4 as uuidv4 } from 'uuid';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
function createMockTag(overrides: Record<string, unknown> = {}) {
return { id: uuidv4(), userId: TEST_USER_ID, name: 'Test', color: '#3B82F6', ...overrides };
}
describe('EventTagController', () => {
let controller: EventTagController;
let service: any;
beforeEach(() => {
service = {
findByUserId: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
controller = new EventTagController(service);
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return all tags', async () => {
const tags = [createMockTag(), createMockTag()];
service.findByUserId.mockResolvedValue(tags);
const result = await controller.findAll(mockUser as any);
expect(result).toEqual({ tags });
});
});
describe('findOne', () => {
it('should return tag by id', async () => {
const tag = createMockTag();
service.findById.mockResolvedValue(tag);
const result = await controller.findOne(mockUser as any, tag.id);
expect(result).toEqual({ tag });
});
it('should throw NotFoundException when not found', async () => {
service.findById.mockResolvedValue(null);
await expect(controller.findOne(mockUser as any, 'bad-id')).rejects.toThrow(
NotFoundException
);
});
});
describe('create', () => {
it('should create tag with userId', async () => {
const tag = createMockTag({ name: 'Work' });
service.create.mockResolvedValue(tag);
const result = await controller.create(
mockUser as any,
{ name: 'Work', color: '#3B82F6' } as any
);
expect(result).toEqual({ tag });
expect(service.create).toHaveBeenCalledWith({
name: 'Work',
color: '#3B82F6',
userId: TEST_USER_ID,
});
});
});
describe('update', () => {
it('should update tag', async () => {
const tag = createMockTag({ name: 'Updated' });
service.update.mockResolvedValue(tag);
const result = await controller.update(mockUser as any, tag.id, { name: 'Updated' } as any);
expect(result).toEqual({ tag });
});
});
describe('delete', () => {
it('should delete tag', async () => {
service.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'tag-id');
expect(result).toEqual({ success: true });
});
});
});

View file

@ -0,0 +1,86 @@
import { EventController } from './event.controller';
import { createMockEvent, TEST_USER_ID } from '../__tests__/utils/mock-factories';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
describe('EventController', () => {
let controller: EventController;
let service: any;
beforeEach(() => {
service = {
getEventsWithCalendar: jest.fn(),
findByIdOrThrow: jest.fn(),
findByCalendar: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
controller = new EventController(service);
});
afterEach(() => jest.clearAllMocks());
describe('queryEvents', () => {
it('should return events with pagination', async () => {
const events = [createMockEvent(), createMockEvent()];
service.getEventsWithCalendar.mockResolvedValue(events);
const query = { limit: 50, offset: 0 } as any;
const result = await controller.queryEvents(mockUser as any, query);
expect(result.events).toEqual(events);
expect(result.pagination).toEqual({ limit: 50, offset: 0, count: 2 });
});
it('should default offset to 0', async () => {
service.getEventsWithCalendar.mockResolvedValue([]);
const result = await controller.queryEvents(mockUser as any, { limit: 50 } as any);
expect(result.pagination.offset).toBe(0);
});
});
describe('findOne', () => {
it('should return event by id', async () => {
const event = createMockEvent();
service.findByIdOrThrow.mockResolvedValue(event);
const result = await controller.findOne(mockUser as any, event.id);
expect(result).toEqual({ event });
});
});
describe('findByCalendar', () => {
it('should return events for calendar', async () => {
const events = [createMockEvent()];
service.findByCalendar.mockResolvedValue(events);
const result = await controller.findByCalendar(mockUser as any, 'cal-id', {} as any);
expect(result).toEqual({ events });
});
});
describe('create', () => {
it('should create event', async () => {
const event = createMockEvent({ title: 'Meeting' });
service.create.mockResolvedValue(event);
const result = await controller.create(mockUser as any, { title: 'Meeting' } as any);
expect(result).toEqual({ event });
});
});
describe('update', () => {
it('should update event', async () => {
const event = createMockEvent({ title: 'Updated' });
service.update.mockResolvedValue(event);
const result = await controller.update(mockUser as any, event.id, {
title: 'Updated',
} as any);
expect(result).toEqual({ event });
});
});
describe('delete', () => {
it('should delete and return success', async () => {
service.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'event-id');
expect(result).toEqual({ success: true });
});
});
});

View file

@ -0,0 +1,132 @@
import { ContactController } from '../contact.controller';
const TEST_USER_ID = 'test-user-123';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
function createMockContact(overrides: Record<string, unknown> = {}) {
return {
id: 'contact-1',
userId: TEST_USER_ID,
firstName: 'Max',
lastName: 'Mustermann',
displayName: 'Max Mustermann',
email: 'max@example.com',
isFavorite: false,
isArchived: false,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
};
}
describe('ContactController', () => {
let controller: ContactController;
let service: any;
beforeEach(() => {
service = {
findByUserId: jest.fn(),
count: jest.fn(),
findWithBirthdays: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
toggleFavorite: jest.fn(),
toggleArchive: jest.fn(),
};
controller = new ContactController(service);
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return contacts with total', async () => {
const contacts = [createMockContact()];
service.findByUserId.mockResolvedValue(contacts);
service.count.mockResolvedValue(1);
const result = await controller.findAll(mockUser as any, {} as any);
expect(result).toEqual({ contacts, total: 1 });
});
});
describe('getBirthdays', () => {
it('should return contacts with birthdays', async () => {
const contacts = [createMockContact({ birthday: '1990-01-15' })];
service.findWithBirthdays.mockResolvedValue(contacts);
const result = await controller.getBirthdays(mockUser as any);
expect(result).toEqual({ contacts });
});
});
describe('findOne', () => {
it('should return contact', async () => {
const contact = createMockContact();
service.findById.mockResolvedValue(contact);
const result = await controller.findOne(mockUser as any, 'contact-1');
expect(result).toEqual({ contact });
});
it('should return null when not found', async () => {
service.findById.mockResolvedValue(null);
const result = await controller.findOne(mockUser as any, 'bad-id');
expect(result).toEqual({ contact: null });
});
});
describe('create', () => {
it('should generate displayName from first+last name', async () => {
const contact = createMockContact();
service.create.mockResolvedValue(contact);
await controller.create(mockUser as any, { firstName: 'Max', lastName: 'Mustermann' } as any);
expect(service.create).toHaveBeenCalledWith(
expect.objectContaining({ displayName: 'Max Mustermann', userId: TEST_USER_ID })
);
});
it('should use provided displayName', async () => {
service.create.mockResolvedValue(createMockContact());
await controller.create(mockUser as any, { displayName: 'Custom' } as any);
expect(service.create).toHaveBeenCalledWith(
expect.objectContaining({ displayName: 'Custom' })
);
});
});
describe('update', () => {
it('should update contact', async () => {
const contact = createMockContact({ firstName: 'Updated' });
service.update.mockResolvedValue(contact);
const result = await controller.update(mockUser as any, 'contact-1', {
firstName: 'Updated',
} as any);
expect(result).toEqual({ contact });
});
});
describe('delete', () => {
it('should delete and return success', async () => {
service.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'contact-1');
expect(result).toEqual({ success: true });
});
});
describe('toggleFavorite', () => {
it('should toggle favorite', async () => {
const contact = createMockContact({ isFavorite: true });
service.toggleFavorite.mockResolvedValue(contact);
const result = await controller.toggleFavorite(mockUser as any, 'contact-1');
expect(result).toEqual({ contact });
});
});
describe('toggleArchive', () => {
it('should toggle archive', async () => {
const contact = createMockContact({ isArchived: true });
service.toggleArchive.mockResolvedValue(contact);
const result = await controller.toggleArchive(mockUser as any, 'contact-1');
expect(result).toEqual({ contact });
});
});
});

View file

@ -0,0 +1,90 @@
import { NotFoundException } from '@nestjs/common';
import { TagController } from '../tag.controller';
const TEST_USER_ID = 'test-user-123';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
function createMockTag(overrides: Record<string, unknown> = {}) {
return { id: 'tag-1', userId: TEST_USER_ID, name: 'Work', color: '#3B82F6', ...overrides };
}
describe('TagController', () => {
let controller: TagController;
let tagService: any;
let contactService: any;
beforeEach(() => {
tagService = {
findByUserId: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
addTagToContact: jest.fn(),
removeTagFromContact: jest.fn(),
getTagsForContact: jest.fn(),
};
contactService = { findById: jest.fn() };
controller = new TagController(tagService, contactService);
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return tags', async () => {
const tags = [createMockTag()];
tagService.findByUserId.mockResolvedValue(tags);
const result = await controller.findAll(mockUser as any);
expect(result).toEqual({ tags });
});
});
describe('create', () => {
it('should create tag with userId', async () => {
const tag = createMockTag();
tagService.create.mockResolvedValue(tag);
await controller.create(mockUser as any, { name: 'Work' } as any);
expect(tagService.create).toHaveBeenCalledWith({ name: 'Work', userId: TEST_USER_ID });
});
});
describe('delete', () => {
it('should delete tag', async () => {
tagService.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'tag-1');
expect(result).toEqual({ success: true });
});
});
describe('addToContact', () => {
it('should add tag to contact', async () => {
tagService.findById.mockResolvedValue(createMockTag());
contactService.findById.mockResolvedValue({ id: 'c1' });
const result = await controller.addToContact(mockUser as any, 'tag-1', 'c1');
expect(result).toEqual({ success: true });
});
it('should throw when tag not found', async () => {
tagService.findById.mockResolvedValue(null);
await expect(controller.addToContact(mockUser as any, 'bad', 'c1')).rejects.toThrow(
NotFoundException
);
});
it('should throw when contact not found', async () => {
tagService.findById.mockResolvedValue(createMockTag());
contactService.findById.mockResolvedValue(null);
await expect(controller.addToContact(mockUser as any, 't1', 'bad')).rejects.toThrow(
NotFoundException
);
});
});
describe('getTagsForContact', () => {
it('should return tag IDs', async () => {
tagService.getTagsForContact.mockResolvedValue(['tag-1']);
const result = await controller.getTagsForContact(mockUser as any, 'c1');
expect(result).toEqual({ tagIds: ['tag-1'] });
});
});
});

View file

@ -0,0 +1,73 @@
import { ProjectController } from '../project.controller';
const TEST_USER_ID = 'test-user-123';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
function createMockProject(overrides: Record<string, unknown> = {}) {
return { id: 'proj-1', userId: TEST_USER_ID, name: 'Test', color: '#3B82F6', ...overrides };
}
describe('ProjectController', () => {
let controller: ProjectController;
let service: any;
beforeEach(() => {
service = {
findAll: jest.fn(),
findByIdOrThrow: jest.fn(),
create: jest.fn(),
getOrCreateDefaultProject: jest.fn().mockResolvedValue(undefined),
update: jest.fn(),
delete: jest.fn(),
archive: jest.fn(),
reorder: jest.fn(),
};
controller = new ProjectController(service);
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return projects', async () => {
const projects = [createMockProject()];
service.findAll.mockResolvedValue(projects);
const result = await controller.findAll(mockUser as any);
expect(result).toEqual({ projects });
});
});
describe('findOne', () => {
it('should return project', async () => {
const project = createMockProject();
service.findByIdOrThrow.mockResolvedValue(project);
const result = await controller.findOne(mockUser as any, 'proj-1');
expect(result).toEqual({ project });
});
});
describe('create', () => {
it('should create project', async () => {
const project = createMockProject({ name: 'New' });
service.create.mockResolvedValue(project);
const result = await controller.create(mockUser as any, { name: 'New' } as any);
expect(result).toEqual({ project });
});
});
describe('delete', () => {
it('should delete', async () => {
service.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'proj-1');
expect(result).toEqual({ success: true });
});
});
describe('archive', () => {
it('should archive project', async () => {
const project = createMockProject({ isArchived: true });
service.archive.mockResolvedValue(project);
const result = await controller.archive(mockUser as any, 'proj-1');
expect(result).toEqual({ project });
});
});
});

View file

@ -0,0 +1,143 @@
import { TaskController } from '../task.controller';
const TEST_USER_ID = 'test-user-123';
const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' };
function createMockTask(overrides: Record<string, unknown> = {}) {
return { id: 'task-1', userId: TEST_USER_ID, title: 'Test', isCompleted: false, ...overrides };
}
describe('TaskController', () => {
let controller: TaskController;
let service: any;
beforeEach(() => {
service = {
findAll: jest.fn(),
getInboxTasks: jest.fn(),
getTodayTasks: jest.fn(),
getUpcomingTasks: jest.fn(),
getCompletedTasks: jest.fn(),
findByContact: jest.fn(),
findByIdOrThrow: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
complete: jest.fn(),
uncomplete: jest.fn(),
move: jest.fn(),
updateTaskLabels: jest.fn(),
};
controller = new TaskController(service);
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return tasks', async () => {
const tasks = [createMockTask()];
service.findAll.mockResolvedValue(tasks);
const result = await controller.findAll(mockUser as any, {} as any);
expect(result).toEqual({ tasks });
});
});
describe('getInbox', () => {
it('should return inbox tasks', async () => {
service.getInboxTasks.mockResolvedValue([createMockTask()]);
const result = await controller.getInbox(mockUser as any);
expect(result.tasks).toHaveLength(1);
});
});
describe('getToday', () => {
it('should return today tasks', async () => {
service.getTodayTasks.mockResolvedValue([]);
const result = await controller.getToday(mockUser as any);
expect(result).toEqual({ tasks: [] });
});
});
describe('getUpcoming', () => {
it('should default to 7 days', async () => {
service.getUpcomingTasks.mockResolvedValue([]);
await controller.getUpcoming(mockUser as any, undefined);
expect(service.getUpcomingTasks).toHaveBeenCalledWith(TEST_USER_ID, 7);
});
it('should use custom days', async () => {
service.getUpcomingTasks.mockResolvedValue([]);
await controller.getUpcoming(mockUser as any, 14);
expect(service.getUpcomingTasks).toHaveBeenCalledWith(TEST_USER_ID, 14);
});
});
describe('getCompleted', () => {
it('should default pagination', async () => {
service.getCompletedTasks.mockResolvedValue({ tasks: [], total: 0 });
await controller.getCompleted(mockUser as any, undefined, undefined);
expect(service.getCompletedTasks).toHaveBeenCalledWith(TEST_USER_ID, 50, 0);
});
});
describe('findOne', () => {
it('should return task', async () => {
const task = createMockTask();
service.findByIdOrThrow.mockResolvedValue(task);
const result = await controller.findOne(mockUser as any, 'task-1');
expect(result).toEqual({ task });
});
});
describe('create', () => {
it('should create task', async () => {
const task = createMockTask({ title: 'New' });
service.create.mockResolvedValue(task);
const result = await controller.create(mockUser as any, { title: 'New' } as any);
expect(result).toEqual({ task });
});
});
describe('delete', () => {
it('should delete and return success', async () => {
service.delete.mockResolvedValue(undefined);
const result = await controller.delete(mockUser as any, 'task-1');
expect(result).toEqual({ success: true });
});
});
describe('complete', () => {
it('should complete task', async () => {
const task = createMockTask({ isCompleted: true });
service.complete.mockResolvedValue(task);
const result = await controller.complete(mockUser as any, 'task-1');
expect(result).toEqual({ task });
});
});
describe('uncomplete', () => {
it('should uncomplete task', async () => {
const task = createMockTask({ isCompleted: false });
service.uncomplete.mockResolvedValue(task);
const result = await controller.uncomplete(mockUser as any, 'task-1');
expect(result).toEqual({ task });
});
});
describe('move', () => {
it('should move task to project', async () => {
const task = createMockTask({ projectId: 'proj-1' });
service.move.mockResolvedValue(task);
const result = await controller.move(mockUser as any, 'task-1', 'proj-1');
expect(result).toEqual({ task });
});
});
describe('getByContact', () => {
it('should return tasks for contact', async () => {
service.findByContact.mockResolvedValue([createMockTask()]);
const result = await controller.getByContact(mockUser as any, 'contact-1', 'false');
expect(result.tasks).toHaveLength(1);
});
});
});