mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
test(contacts): add 34 backend unit tests for tags, notes, activities, duplicates
- tag.service.spec.ts: 10 tests (CRUD, default creation, contact associations) - note.service.spec.ts: 10 tests (CRUD, togglePin, ordering) - activity.service.spec.ts: 7 tests (findByContact, create, logActivity) - duplicates.service.spec.ts: 8 tests (findDuplicates, mergeContacts, dismiss) Total: 55 backend tests (was 15) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f53e460ef0
commit
5345e19e24
4 changed files with 833 additions and 0 deletions
|
|
@ -0,0 +1,173 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ActivityService } from '../activity.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
describe('ActivityService', () => {
|
||||
let service: ActivityService;
|
||||
let mockDb: any;
|
||||
|
||||
const mockActivity = {
|
||||
id: 'activity-1',
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
activityType: 'created',
|
||||
description: 'Contact created',
|
||||
metadata: null,
|
||||
createdAt: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
const mockActivity2 = {
|
||||
id: 'activity-2',
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
activityType: 'called',
|
||||
description: 'Called contact',
|
||||
metadata: { duration: '5m' },
|
||||
createdAt: new Date('2025-01-02'),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: 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: [
|
||||
ActivityService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ActivityService>(ActivityService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findByContactId', () => {
|
||||
it('should return activities for a contact ordered by date descending', async () => {
|
||||
mockDb.limit.mockResolvedValue([mockActivity2, mockActivity]);
|
||||
|
||||
const result = await service.findByContactId('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual([mockActivity2, mockActivity]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
expect(mockDb.orderBy).toHaveBeenCalled();
|
||||
expect(mockDb.limit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when contact has no activities', async () => {
|
||||
mockDb.limit.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findByContactId('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should respect custom limit parameter', async () => {
|
||||
mockDb.limit.mockResolvedValue([mockActivity2]);
|
||||
|
||||
const result = await service.findByContactId('contact-1', 'user-1', 1);
|
||||
|
||||
expect(result).toEqual([mockActivity2]);
|
||||
expect(mockDb.limit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use default limit of 50', async () => {
|
||||
mockDb.limit.mockResolvedValue([]);
|
||||
|
||||
await service.findByContactId('contact-1', 'user-1');
|
||||
|
||||
expect(mockDb.limit).toHaveBeenCalledWith(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should insert and return a new activity', async () => {
|
||||
mockDb.returning.mockResolvedValue([mockActivity]);
|
||||
|
||||
const newActivity = {
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
activityType: 'created',
|
||||
description: 'Contact created',
|
||||
};
|
||||
|
||||
const result = await service.create(newActivity as any);
|
||||
|
||||
expect(result).toEqual(mockActivity);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalledWith(newActivity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logActivity', () => {
|
||||
it('should log an activity with description', async () => {
|
||||
mockDb.returning.mockResolvedValue([mockActivity2]);
|
||||
|
||||
const result = await service.logActivity('contact-1', 'user-1', 'called', 'Called contact');
|
||||
|
||||
expect(result).toEqual(mockActivity2);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalledWith({
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
activityType: 'called',
|
||||
description: 'Called contact',
|
||||
metadata: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should log an activity with metadata', async () => {
|
||||
const activityWithMeta = {
|
||||
...mockActivity2,
|
||||
metadata: { duration: '5m' },
|
||||
};
|
||||
mockDb.returning.mockResolvedValue([activityWithMeta]);
|
||||
|
||||
const result = await service.logActivity('contact-1', 'user-1', 'called', 'Called contact', {
|
||||
duration: '5m',
|
||||
});
|
||||
|
||||
expect(result).toEqual(activityWithMeta);
|
||||
expect(mockDb.values).toHaveBeenCalledWith({
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
activityType: 'called',
|
||||
description: 'Called contact',
|
||||
metadata: { duration: '5m' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should log an activity without description or metadata', async () => {
|
||||
mockDb.returning.mockResolvedValue([mockActivity]);
|
||||
|
||||
const result = await service.logActivity('contact-1', 'user-1', 'created');
|
||||
|
||||
expect(result).toEqual(mockActivity);
|
||||
expect(mockDb.values).toHaveBeenCalledWith({
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
activityType: 'created',
|
||||
description: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { DuplicatesService } from '../duplicates.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
describe('DuplicatesService', () => {
|
||||
let service: DuplicatesService;
|
||||
let mockDb: any;
|
||||
|
||||
const mockContact1 = {
|
||||
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: 'Note 1',
|
||||
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'),
|
||||
};
|
||||
|
||||
const mockContact2 = {
|
||||
id: 'contact-2',
|
||||
userId: 'user-1',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
displayName: 'John D.',
|
||||
email: 'john@example.com',
|
||||
phone: null,
|
||||
mobile: '+9876543210',
|
||||
nickname: null,
|
||||
street: '123 Main St',
|
||||
city: 'Springfield',
|
||||
postalCode: null,
|
||||
country: null,
|
||||
company: null,
|
||||
jobTitle: 'Developer',
|
||||
department: null,
|
||||
website: 'https://john.dev',
|
||||
birthday: null,
|
||||
notes: 'Note 2',
|
||||
photoUrl: null,
|
||||
isFavorite: true,
|
||||
isArchived: false,
|
||||
organizationId: null,
|
||||
teamId: null,
|
||||
visibility: 'private',
|
||||
sharedWith: null,
|
||||
createdAt: new Date('2025-01-02'),
|
||||
updatedAt: new Date('2025-01-02'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a fresh chainable mock that tracks call order.
|
||||
* Each terminal call (limit, where as terminal, returning, execute)
|
||||
* can be configured to resolve with specific values per invocation.
|
||||
*/
|
||||
function createMockDb() {
|
||||
const db: any = {
|
||||
select: jest.fn(),
|
||||
from: jest.fn(),
|
||||
where: jest.fn(),
|
||||
limit: jest.fn(),
|
||||
orderBy: jest.fn(),
|
||||
groupBy: jest.fn(),
|
||||
having: jest.fn(),
|
||||
insert: jest.fn(),
|
||||
values: jest.fn(),
|
||||
returning: jest.fn(),
|
||||
update: jest.fn(),
|
||||
set: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
// All methods return the db itself for chaining by default
|
||||
for (const key of Object.keys(db)) {
|
||||
db[key].mockReturnValue(db);
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = createMockDb();
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DuplicatesService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DuplicatesService>(DuplicatesService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findDuplicates', () => {
|
||||
it('should return empty array when no duplicates found', async () => {
|
||||
// Email: select().from().where().groupBy().having().limit() -> []
|
||||
mockDb.limit.mockResolvedValueOnce([]);
|
||||
// Phone: execute() -> []
|
||||
mockDb.execute.mockResolvedValueOnce([]);
|
||||
// Name: select().from().where().groupBy().having().limit() -> []
|
||||
mockDb.limit.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.findDuplicates('user-1');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should find email duplicates', async () => {
|
||||
// Email grouping query: limit() returns duplicate email groups
|
||||
mockDb.limit.mockResolvedValueOnce([{ normalizedEmail: 'john@example.com' }]);
|
||||
// Fetch contacts matching those emails: where() returns contacts
|
||||
// Since where() is the terminal here, we need it to resolve after the groupBy chain
|
||||
// The second where() call (after the limit resolved) returns contacts
|
||||
mockDb.where
|
||||
.mockReturnValueOnce(mockDb) // first where in groupBy chain
|
||||
.mockResolvedValueOnce([mockContact1, mockContact2]); // second where fetches contacts
|
||||
// Phone duplicates: execute() -> []
|
||||
mockDb.execute.mockResolvedValueOnce([]);
|
||||
// Name duplicates: limit() -> []
|
||||
mockDb.limit.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.findDuplicates('user-1');
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].matchType).toBe('email');
|
||||
expect(result[0].matchValue).toBe('john@example.com');
|
||||
expect(result[0].contacts).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeContacts', () => {
|
||||
it('should merge contacts and return merged result', async () => {
|
||||
const mergedContact = {
|
||||
...mockContact1,
|
||||
mobile: '+9876543210',
|
||||
street: '123 Main St',
|
||||
city: 'Springfield',
|
||||
jobTitle: 'Developer',
|
||||
website: 'https://john.dev',
|
||||
notes: 'Note 1\n\n---\n\nNote 2',
|
||||
isFavorite: true,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Get primary contact: select().from().where() -> [mockContact1]
|
||||
mockDb.where.mockResolvedValueOnce([mockContact1]);
|
||||
// Get contacts to merge: select().from().where() -> [mockContact2]
|
||||
mockDb.where.mockResolvedValueOnce([mockContact2]);
|
||||
// Update primary: update().set().where() -> chainable (returns mockDb)
|
||||
mockDb.where.mockReturnValueOnce(mockDb);
|
||||
// update chain .returning() -> [mergedContact]
|
||||
mockDb.returning.mockResolvedValueOnce([mergedContact]);
|
||||
// Delete merged: delete().where() -> undefined
|
||||
mockDb.where.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await service.mergeContacts('contact-1', ['contact-2'], 'user-1');
|
||||
|
||||
expect(result.mergedContact).toEqual(mergedContact);
|
||||
expect(result.deletedIds).toEqual(['contact-2']);
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when primary contact is not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.mergeContacts('nonexistent', ['contact-2'], 'user-1')).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when merge contacts are not found', async () => {
|
||||
// Primary contact found
|
||||
mockDb.where.mockResolvedValueOnce([mockContact1]);
|
||||
// Merge contacts not found (empty for 2 requested IDs)
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
service.mergeContacts('contact-1', ['contact-2', 'contact-3'], 'user-1')
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when some merge contacts are missing', async () => {
|
||||
// Primary contact found
|
||||
mockDb.where.mockResolvedValueOnce([mockContact1]);
|
||||
// Only 1 of 2 merge contacts found
|
||||
mockDb.where.mockResolvedValueOnce([mockContact2]);
|
||||
|
||||
await expect(
|
||||
service.mergeContacts('contact-1', ['contact-2', 'contact-3'], 'user-1')
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should keep favorite status if any merged contact is favorite', async () => {
|
||||
const mergedContact = {
|
||||
...mockContact1,
|
||||
isFavorite: true,
|
||||
};
|
||||
|
||||
// Primary (not favorite)
|
||||
mockDb.where.mockResolvedValueOnce([mockContact1]);
|
||||
// Merge contacts (contact2 is favorite)
|
||||
mockDb.where.mockResolvedValueOnce([mockContact2]);
|
||||
// Update chain: where() returns chainable
|
||||
mockDb.where.mockReturnValueOnce(mockDb);
|
||||
// Update returning
|
||||
mockDb.returning.mockResolvedValueOnce([mergedContact]);
|
||||
// Delete
|
||||
mockDb.where.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await service.mergeContacts('contact-1', ['contact-2'], 'user-1');
|
||||
|
||||
expect(result.mergedContact.isFavorite).toBe(true);
|
||||
});
|
||||
|
||||
it('should fill empty primary fields from merged contacts', async () => {
|
||||
const primaryWithGaps = {
|
||||
...mockContact1,
|
||||
street: null,
|
||||
city: null,
|
||||
jobTitle: null,
|
||||
website: null,
|
||||
mobile: null,
|
||||
};
|
||||
const mergedResult = {
|
||||
...primaryWithGaps,
|
||||
street: '123 Main St',
|
||||
city: 'Springfield',
|
||||
jobTitle: 'Developer',
|
||||
website: 'https://john.dev',
|
||||
mobile: '+9876543210',
|
||||
};
|
||||
|
||||
mockDb.where.mockResolvedValueOnce([primaryWithGaps]);
|
||||
mockDb.where.mockResolvedValueOnce([mockContact2]);
|
||||
mockDb.where.mockReturnValueOnce(mockDb);
|
||||
mockDb.returning.mockResolvedValueOnce([mergedResult]);
|
||||
mockDb.where.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await service.mergeContacts('contact-1', ['contact-2'], 'user-1');
|
||||
|
||||
expect(result.mergedContact.street).toBe('123 Main St');
|
||||
expect(result.mergedContact.city).toBe('Springfield');
|
||||
expect(result.mergedContact.jobTitle).toBe('Developer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissDuplicate', () => {
|
||||
it('should resolve without error (no-op for now)', async () => {
|
||||
await expect(
|
||||
service.dismissDuplicate('email-contact-1-contact-2', 'user-1')
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { NoteService } from '../note.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
describe('NoteService', () => {
|
||||
let service: NoteService;
|
||||
let mockDb: any;
|
||||
|
||||
const mockNote = {
|
||||
id: 'note-1',
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
content: 'Test note content',
|
||||
isPinned: false,
|
||||
createdAt: new Date('2025-01-01'),
|
||||
updatedAt: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
const mockNote2 = {
|
||||
id: 'note-2',
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
content: 'Another note',
|
||||
isPinned: true,
|
||||
createdAt: new Date('2025-01-02'),
|
||||
updatedAt: new Date('2025-01-02'),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: 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: [
|
||||
NoteService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<NoteService>(NoteService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findByContactId', () => {
|
||||
it('should return notes for a contact ordered by pinned and date', async () => {
|
||||
mockDb.orderBy.mockResolvedValue([mockNote2, mockNote]);
|
||||
|
||||
const result = await service.findByContactId('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual([mockNote2, mockNote]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
expect(mockDb.orderBy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when contact has no notes', async () => {
|
||||
mockDb.orderBy.mockResolvedValue([]);
|
||||
|
||||
const result = await service.findByContactId('contact-1', 'user-1');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a note when found', async () => {
|
||||
mockDb.where.mockResolvedValue([mockNote]);
|
||||
|
||||
const result = await service.findById('note-1', 'user-1');
|
||||
|
||||
expect(result).toEqual(mockNote);
|
||||
});
|
||||
|
||||
it('should return null when note 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 note', async () => {
|
||||
mockDb.returning.mockResolvedValue([mockNote]);
|
||||
|
||||
const newNote = {
|
||||
contactId: 'contact-1',
|
||||
userId: 'user-1',
|
||||
content: 'Test note content',
|
||||
};
|
||||
|
||||
const result = await service.create(newNote as any);
|
||||
|
||||
expect(result).toEqual(mockNote);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalledWith(newNote);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update and return the note', async () => {
|
||||
const updatedNote = { ...mockNote, content: 'Updated content' };
|
||||
mockDb.returning.mockResolvedValue([updatedNote]);
|
||||
|
||||
const result = await service.update('note-1', 'user-1', { content: 'Updated content' });
|
||||
|
||||
expect(result).toEqual(updatedNote);
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when note is not found', async () => {
|
||||
mockDb.returning.mockResolvedValue([]);
|
||||
|
||||
await expect(service.update('nonexistent', 'user-1', { content: 'Updated' })).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a note successfully', async () => {
|
||||
mockDb.where.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.delete('note-1', 'user-1')).resolves.toBeUndefined();
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('togglePin', () => {
|
||||
it('should toggle isPinned from false to true', async () => {
|
||||
const pinnedNote = { ...mockNote, isPinned: true };
|
||||
// findById call
|
||||
mockDb.where.mockResolvedValueOnce([mockNote]);
|
||||
// update call (inside this.update)
|
||||
mockDb.returning.mockResolvedValue([pinnedNote]);
|
||||
|
||||
const result = await service.togglePin('note-1', 'user-1');
|
||||
|
||||
expect(result).toEqual(pinnedNote);
|
||||
expect(result.isPinned).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle isPinned from true to false', async () => {
|
||||
const unpinnedNote = { ...mockNote2, isPinned: false };
|
||||
// findById call
|
||||
mockDb.where.mockResolvedValueOnce([mockNote2]);
|
||||
// update call
|
||||
mockDb.returning.mockResolvedValue([unpinnedNote]);
|
||||
|
||||
const result = await service.togglePin('note-2', 'user-1');
|
||||
|
||||
expect(result).toEqual(unpinnedNote);
|
||||
expect(result.isPinned).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when note is not found', async () => {
|
||||
mockDb.where.mockResolvedValue([]);
|
||||
|
||||
await expect(service.togglePin('nonexistent', 'user-1')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
193
apps/contacts/apps/backend/src/tag/__tests__/tag.service.spec.ts
Normal file
193
apps/contacts/apps/backend/src/tag/__tests__/tag.service.spec.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { TagService } from '../tag.service';
|
||||
import { DATABASE_CONNECTION } from '../../db/database.module';
|
||||
|
||||
describe('TagService', () => {
|
||||
let service: TagService;
|
||||
let mockDb: any;
|
||||
|
||||
const mockTag = {
|
||||
id: 'tag-1',
|
||||
userId: 'user-1',
|
||||
name: 'Familie',
|
||||
color: '#ec4899',
|
||||
createdAt: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
const mockTag2 = {
|
||||
id: 'tag-2',
|
||||
userId: 'user-1',
|
||||
name: 'Freunde',
|
||||
color: '#22c55e',
|
||||
createdAt: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: 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(),
|
||||
onConflictDoNothing: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TagService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TagService>(TagService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findByUserId', () => {
|
||||
it('should return existing tags when user has tags', async () => {
|
||||
mockDb.where.mockResolvedValue([mockTag, mockTag2]);
|
||||
|
||||
const result = await service.findByUserId('user-1');
|
||||
|
||||
expect(result).toEqual([mockTag, mockTag2]);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create default tags when user has no tags', async () => {
|
||||
const defaultTags = [
|
||||
{ id: 'tag-a', userId: 'user-1', name: 'Familie', color: '#ec4899' },
|
||||
{ id: 'tag-b', userId: 'user-1', name: 'Freunde', color: '#22c55e' },
|
||||
{ id: 'tag-c', userId: 'user-1', name: 'Arbeit', color: '#3b82f6' },
|
||||
{ id: 'tag-d', userId: 'user-1', name: 'Wichtig', color: '#ef4444' },
|
||||
];
|
||||
// First call: findByUserId returns empty
|
||||
mockDb.where.mockResolvedValue([]);
|
||||
// Second call: createDefaultTags inserts and returns
|
||||
mockDb.returning.mockResolvedValue(defaultTags);
|
||||
|
||||
const result = await service.findByUserId('user-1');
|
||||
|
||||
expect(result).toEqual(defaultTags);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalled();
|
||||
expect(mockDb.returning).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return a tag when found', async () => {
|
||||
mockDb.where.mockResolvedValue([mockTag]);
|
||||
|
||||
const result = await service.findById('tag-1', 'user-1');
|
||||
|
||||
expect(result).toEqual(mockTag);
|
||||
});
|
||||
|
||||
it('should return null when tag 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 tag', async () => {
|
||||
mockDb.returning.mockResolvedValue([mockTag]);
|
||||
|
||||
const newTag = {
|
||||
userId: 'user-1',
|
||||
name: 'Familie',
|
||||
color: '#ec4899',
|
||||
};
|
||||
|
||||
const result = await service.create(newTag as any);
|
||||
|
||||
expect(result).toEqual(mockTag);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalledWith(newTag);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update and return the tag', async () => {
|
||||
const updatedTag = { ...mockTag, name: 'Updated Name' };
|
||||
mockDb.returning.mockResolvedValue([updatedTag]);
|
||||
|
||||
const result = await service.update('tag-1', 'user-1', { name: 'Updated Name' });
|
||||
|
||||
expect(result).toEqual(updatedTag);
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when tag is not found', async () => {
|
||||
mockDb.returning.mockResolvedValue([]);
|
||||
|
||||
await expect(service.update('nonexistent', 'user-1', { name: 'Test' })).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a tag successfully', async () => {
|
||||
mockDb.where.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.delete('tag-1', 'user-1')).resolves.toBeUndefined();
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addTagToContact', () => {
|
||||
it('should add a tag to a contact', async () => {
|
||||
mockDb.values.mockReturnValue({ onConflictDoNothing: mockDb.onConflictDoNothing });
|
||||
|
||||
await expect(service.addTagToContact('contact-1', 'tag-1')).resolves.toBeUndefined();
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTagFromContact', () => {
|
||||
it('should remove a tag from a contact', async () => {
|
||||
mockDb.where.mockResolvedValue(undefined);
|
||||
|
||||
await expect(service.removeTagFromContact('contact-1', 'tag-1')).resolves.toBeUndefined();
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsForContact', () => {
|
||||
it('should return tag IDs for a contact', async () => {
|
||||
mockDb.where.mockResolvedValue([{ tagId: 'tag-1' }, { tagId: 'tag-2' }]);
|
||||
|
||||
const result = await service.getTagsForContact('contact-1');
|
||||
|
||||
expect(result).toEqual(['tag-1', 'tag-2']);
|
||||
});
|
||||
|
||||
it('should return empty array when contact has no tags', async () => {
|
||||
mockDb.where.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getTagsForContact('contact-1');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue