mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:01:09 +02:00
test(calendar): add 69 backend unit tests for event-tags, sync, and notifications
- event-tag.service.spec.ts: 13 tests (CRUD, event associations, group assignment) - event-tag-group.service.spec.ts: 13 tests (CRUD, default creation, reorder) - sync.service.spec.ts: 32 tests (iCal/CalDAV/Google connect, disconnect, sync, export) - notification.service.spec.ts: 11 tests (register/remove token, send push, deactivation) Total: 132 backend tests (was 63) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2ea7bb7a18
commit
f53e460ef0
4 changed files with 1201 additions and 0 deletions
|
|
@ -0,0 +1,279 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { EventTagGroupService } from './event-tag-group.service';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { TEST_USER_ID } from '../__tests__/utils/mock-factories';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
function createMockEventTagGroup(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
userId: TEST_USER_ID,
|
||||
name: 'Test Group',
|
||||
color: '#3B82F6',
|
||||
sortOrder: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('EventTagGroupService', () => {
|
||||
let service: EventTagGroupService;
|
||||
let mockDb: any;
|
||||
|
||||
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().mockResolvedValue([]),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EventTagGroupService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EventTagGroupService>(EventTagGroupService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findByUserId', () => {
|
||||
it('should return all groups for a user ordered by sortOrder', async () => {
|
||||
const groups = [
|
||||
createMockEventTagGroup({ name: 'Group 1', sortOrder: 0 }),
|
||||
createMockEventTagGroup({ name: 'Group 2', sortOrder: 1 }),
|
||||
];
|
||||
mockDb.orderBy.mockResolvedValueOnce(groups);
|
||||
|
||||
const result = await service.findByUserId(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(groups);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
expect(mockDb.orderBy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create default groups when user has no groups', async () => {
|
||||
const defaultGroups = [
|
||||
createMockEventTagGroup({ name: 'Personen', color: '#ec4899', sortOrder: 0 }),
|
||||
createMockEventTagGroup({ name: 'Orte', color: '#14b8a6', sortOrder: 1 }),
|
||||
createMockEventTagGroup({ name: 'Allgemein', color: '#3b82f6', sortOrder: 2 }),
|
||||
];
|
||||
// First call returns empty (no groups yet)
|
||||
mockDb.orderBy.mockResolvedValueOnce([]);
|
||||
// createDefaultGroups calls insert().values().returning()
|
||||
mockDb.returning.mockResolvedValueOnce(defaultGroups);
|
||||
|
||||
const result = await service.findByUserId(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(defaultGroups);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return group when found', async () => {
|
||||
const group = createMockEventTagGroup();
|
||||
mockDb.where.mockResolvedValueOnce([group]);
|
||||
|
||||
const result = await service.findById(group.id, TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(group);
|
||||
});
|
||||
|
||||
it('should return null when group not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.findById('non-existent-id', TEST_USER_ID);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new group with correct sortOrder', async () => {
|
||||
const existingGroups = [
|
||||
createMockEventTagGroup({ sortOrder: 0 }),
|
||||
createMockEventTagGroup({ sortOrder: 1 }),
|
||||
];
|
||||
const newGroup = createMockEventTagGroup({ name: 'New Group', sortOrder: 2 });
|
||||
|
||||
// First call: get existing groups to determine sortOrder
|
||||
mockDb.where.mockResolvedValueOnce(existingGroups);
|
||||
// Second call: insert returning
|
||||
mockDb.returning.mockResolvedValueOnce([newGroup]);
|
||||
|
||||
const result = await service.create({
|
||||
userId: TEST_USER_ID,
|
||||
name: 'New Group',
|
||||
color: '#FF0000',
|
||||
});
|
||||
|
||||
expect(result).toEqual(newGroup);
|
||||
expect(result.sortOrder).toBe(2);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should start with sortOrder 0 when no groups exist', async () => {
|
||||
const newGroup = createMockEventTagGroup({ name: 'First Group', sortOrder: 0 });
|
||||
|
||||
// No existing groups
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
mockDb.returning.mockResolvedValueOnce([newGroup]);
|
||||
|
||||
const result = await service.create({
|
||||
userId: TEST_USER_ID,
|
||||
name: 'First Group',
|
||||
color: '#FF0000',
|
||||
});
|
||||
|
||||
expect(result).toEqual(newGroup);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a group', async () => {
|
||||
const updatedGroup = createMockEventTagGroup({ name: 'Updated Group' });
|
||||
mockDb.returning.mockResolvedValueOnce([updatedGroup]);
|
||||
|
||||
const result = await service.update(updatedGroup.id, TEST_USER_ID, {
|
||||
name: 'Updated Group',
|
||||
});
|
||||
|
||||
expect(result.name).toBe('Updated Group');
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when group not found', async () => {
|
||||
mockDb.returning.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
service.update('non-existent-id', TEST_USER_ID, { name: 'New Name' })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should unassign tags and delete the group', async () => {
|
||||
const groupId = uuidv4();
|
||||
|
||||
await service.delete(groupId, TEST_USER_ID);
|
||||
|
||||
// Should update tags to unassign from group first
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalledWith({ groupId: null });
|
||||
// Should then delete the group
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagCountByGroup', () => {
|
||||
it('should return count of tags in a group', async () => {
|
||||
const groupId = uuidv4();
|
||||
const tags = [
|
||||
{ id: uuidv4(), groupId },
|
||||
{ id: uuidv4(), groupId },
|
||||
{ id: uuidv4(), groupId },
|
||||
];
|
||||
mockDb.where.mockResolvedValueOnce(tags);
|
||||
|
||||
const result = await service.getTagCountByGroup(groupId);
|
||||
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it('should return 0 when group has no tags', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getTagCountByGroup(uuidv4());
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagCountsForUser', () => {
|
||||
it('should return tag counts grouped by groupId', async () => {
|
||||
const groupId1 = uuidv4();
|
||||
const groupId2 = uuidv4();
|
||||
const tags = [
|
||||
{ id: uuidv4(), groupId: groupId1 },
|
||||
{ id: uuidv4(), groupId: groupId1 },
|
||||
{ id: uuidv4(), groupId: groupId2 },
|
||||
{ id: uuidv4(), groupId: null },
|
||||
];
|
||||
mockDb.where.mockResolvedValueOnce(tags);
|
||||
|
||||
const result = await service.getTagCountsForUser(TEST_USER_ID);
|
||||
|
||||
expect(result.get(groupId1)).toBe(2);
|
||||
expect(result.get(groupId2)).toBe(1);
|
||||
expect(result.get(null)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return empty map when user has no tags', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getTagCountsForUser(TEST_USER_ID);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorder', () => {
|
||||
it('should update sortOrder for each group and return updated list', async () => {
|
||||
const groupId1 = uuidv4();
|
||||
const groupId2 = uuidv4();
|
||||
const groupId3 = uuidv4();
|
||||
|
||||
const reorderedGroups = [
|
||||
createMockEventTagGroup({ id: groupId2, sortOrder: 0 }),
|
||||
createMockEventTagGroup({ id: groupId3, sortOrder: 1 }),
|
||||
createMockEventTagGroup({ id: groupId1, sortOrder: 2 }),
|
||||
];
|
||||
|
||||
// The reorder method calls findByUserId at the end, which calls orderBy
|
||||
mockDb.orderBy.mockResolvedValueOnce(reorderedGroups);
|
||||
|
||||
const result = await service.reorder(TEST_USER_ID, [groupId2, groupId3, groupId1]);
|
||||
|
||||
expect(result).toEqual(reorderedGroups);
|
||||
// Should have called update for each group
|
||||
expect(mockDb.update).toHaveBeenCalledTimes(3);
|
||||
expect(mockDb.set).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle empty groupIds array', async () => {
|
||||
const groups = [createMockEventTagGroup()];
|
||||
// findByUserId is called at the end
|
||||
mockDb.orderBy.mockResolvedValueOnce(groups);
|
||||
|
||||
const result = await service.reorder(TEST_USER_ID, []);
|
||||
|
||||
expect(result).toEqual(groups);
|
||||
// No update calls for empty array
|
||||
expect(mockDb.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { EventTagService } from './event-tag.service';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { TEST_USER_ID } from '../__tests__/utils/mock-factories';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
function createMockEventTag(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
userId: TEST_USER_ID,
|
||||
name: 'Test Tag',
|
||||
color: '#3B82F6',
|
||||
groupId: null,
|
||||
sortOrder: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('EventTagService', () => {
|
||||
let service: EventTagService;
|
||||
let mockDb: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDb = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
innerJoin: jest.fn().mockReturnThis(),
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn().mockResolvedValue([]),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
onConflictDoNothing: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EventTagService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EventTagService>(EventTagService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findByUserId', () => {
|
||||
it('should return all tags for a user', async () => {
|
||||
const tags = [createMockEventTag({ name: 'Work' }), createMockEventTag({ name: 'Personal' })];
|
||||
mockDb.where.mockResolvedValueOnce(tags);
|
||||
|
||||
const result = await service.findByUserId(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(tags);
|
||||
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 = [
|
||||
createMockEventTag({ name: 'Arbeit', color: '#3b82f6' }),
|
||||
createMockEventTag({ name: 'Persönlich', color: '#22c55e' }),
|
||||
createMockEventTag({ name: 'Familie', color: '#ec4899' }),
|
||||
createMockEventTag({ name: 'Wichtig', color: '#ef4444' }),
|
||||
];
|
||||
// First call returns empty (no tags yet)
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
// createDefaultTags calls insert().values().returning()
|
||||
mockDb.returning.mockResolvedValueOnce(defaultTags);
|
||||
|
||||
const result = await service.findByUserId(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(defaultTags);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalled();
|
||||
expect(mockDb.returning).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return tag when found', async () => {
|
||||
const tag = createMockEventTag();
|
||||
mockDb.where.mockResolvedValueOnce([tag]);
|
||||
|
||||
const result = await service.findById(tag.id, TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(tag);
|
||||
});
|
||||
|
||||
it('should return null when tag not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.findById('non-existent-id', TEST_USER_ID);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new tag', async () => {
|
||||
const newTag = createMockEventTag({ name: 'New Tag', color: '#FF0000' });
|
||||
mockDb.returning.mockResolvedValueOnce([newTag]);
|
||||
|
||||
const result = await service.create({
|
||||
userId: TEST_USER_ID,
|
||||
name: 'New Tag',
|
||||
color: '#FF0000',
|
||||
});
|
||||
|
||||
expect(result).toEqual(newTag);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a tag', async () => {
|
||||
const updatedTag = createMockEventTag({ name: 'Updated Tag' });
|
||||
mockDb.returning.mockResolvedValueOnce([updatedTag]);
|
||||
|
||||
const result = await service.update(updatedTag.id, TEST_USER_ID, {
|
||||
name: 'Updated Tag',
|
||||
});
|
||||
|
||||
expect(result.name).toBe('Updated Tag');
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when tag not found', async () => {
|
||||
mockDb.returning.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
service.update('non-existent-id', TEST_USER_ID, { name: 'New Name' })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a tag', async () => {
|
||||
const tag = createMockEventTag();
|
||||
|
||||
await service.delete(tag.id, TEST_USER_ID);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsForEvent', () => {
|
||||
it('should return tags for an event', async () => {
|
||||
const tag1 = createMockEventTag({ name: 'Tag 1' });
|
||||
const tag2 = createMockEventTag({ name: 'Tag 2' });
|
||||
mockDb.where.mockResolvedValueOnce([{ tag: tag1 }, { tag: tag2 }]);
|
||||
|
||||
const result = await service.getTagsForEvent('event-id');
|
||||
|
||||
expect(result).toEqual([tag1, tag2]);
|
||||
});
|
||||
|
||||
it('should return empty array when event has no tags', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getTagsForEvent('event-id');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsForEvents', () => {
|
||||
it('should return empty map for empty eventIds', async () => {
|
||||
const result = await service.getTagsForEvents([]);
|
||||
|
||||
expect(result).toEqual(new Map());
|
||||
expect(mockDb.select).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return tags grouped by event id', async () => {
|
||||
const tag1 = createMockEventTag({ name: 'Tag 1' });
|
||||
const tag2 = createMockEventTag({ name: 'Tag 2' });
|
||||
mockDb.where.mockResolvedValueOnce([
|
||||
{ eventId: 'event-1', tag: tag1 },
|
||||
{ eventId: 'event-1', tag: tag2 },
|
||||
{ eventId: 'event-2', tag: tag1 },
|
||||
]);
|
||||
|
||||
const result = await service.getTagsForEvents(['event-1', 'event-2']);
|
||||
|
||||
expect(result.get('event-1')).toEqual([tag1, tag2]);
|
||||
expect(result.get('event-2')).toEqual([tag1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagIdsForEvent', () => {
|
||||
it('should return tag ids for an event', async () => {
|
||||
const tagId1 = uuidv4();
|
||||
const tagId2 = uuidv4();
|
||||
mockDb.where.mockResolvedValueOnce([{ tagId: tagId1 }, { tagId: tagId2 }]);
|
||||
|
||||
const result = await service.getTagIdsForEvent('event-id');
|
||||
|
||||
expect(result).toEqual([tagId1, tagId2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEventTags', () => {
|
||||
it('should remove existing tags and add new ones', async () => {
|
||||
const tagId1 = uuidv4();
|
||||
const tagId2 = uuidv4();
|
||||
|
||||
await service.setEventTags('event-id', [tagId1, tagId2]);
|
||||
|
||||
// Should delete existing tags first
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
// Should insert new tags
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only remove tags when tagIds is empty', async () => {
|
||||
await service.setEventTags('event-id', []);
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
// insert should not be called for empty tagIds
|
||||
expect(mockDb.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addTagToEvent', () => {
|
||||
it('should add a tag to an event', async () => {
|
||||
await service.addTagToEvent('event-id', 'tag-id');
|
||||
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalledWith({ eventId: 'event-id', tagId: 'tag-id' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTagFromEvent', () => {
|
||||
it('should remove a tag from an event', async () => {
|
||||
await service.removeTagFromEvent('event-id', 'tag-id');
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsByIds', () => {
|
||||
it('should return empty array for empty ids', async () => {
|
||||
const result = await service.getTagsByIds([], TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockDb.select).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return tags matching ids', async () => {
|
||||
const tag1 = createMockEventTag({ name: 'Tag 1' });
|
||||
const tag2 = createMockEventTag({ name: 'Tag 2' });
|
||||
mockDb.where.mockResolvedValueOnce([tag1, tag2]);
|
||||
|
||||
const result = await service.getTagsByIds([tag1.id, tag2.id], TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual([tag1, tag2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByGroupId', () => {
|
||||
it('should return tags for a specific group', async () => {
|
||||
const groupId = uuidv4();
|
||||
const tags = [
|
||||
createMockEventTag({ name: 'Tag 1', groupId }),
|
||||
createMockEventTag({ name: 'Tag 2', groupId }),
|
||||
];
|
||||
mockDb.orderBy.mockResolvedValueOnce(tags);
|
||||
|
||||
const result = await service.findByGroupId(groupId, TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(tags);
|
||||
});
|
||||
|
||||
it('should return ungrouped tags when groupId is null', async () => {
|
||||
const tags = [createMockEventTag({ name: 'Ungrouped', groupId: null })];
|
||||
mockDb.orderBy.mockResolvedValueOnce(tags);
|
||||
|
||||
const result = await service.findByGroupId(null, TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(tags);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTagGroup', () => {
|
||||
it('should update the group of a tag', async () => {
|
||||
const groupId = uuidv4();
|
||||
const updatedTag = createMockEventTag({ groupId });
|
||||
mockDb.returning.mockResolvedValueOnce([updatedTag]);
|
||||
|
||||
const result = await service.updateTagGroup(updatedTag.id, TEST_USER_ID, groupId);
|
||||
|
||||
expect(result.groupId).toBe(groupId);
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when tag not found', async () => {
|
||||
mockDb.returning.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.updateTagGroup('non-existent-id', TEST_USER_ID, null)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotificationService } from './notification.service';
|
||||
import { PushService } from './push.service';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { createMockDeviceToken, TEST_USER_ID } from '../__tests__/utils/mock-factories';
|
||||
|
||||
describe('NotificationService', () => {
|
||||
let service: NotificationService;
|
||||
let mockDb: any;
|
||||
let mockPushService: any;
|
||||
|
||||
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().mockResolvedValue([]),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
mockPushService = {
|
||||
sendToToken: jest.fn().mockResolvedValue(true),
|
||||
sendToTokens: jest.fn().mockResolvedValue(new Map()),
|
||||
isValidToken: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
NotificationService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
{
|
||||
provide: PushService,
|
||||
useValue: mockPushService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<NotificationService>(NotificationService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('registerToken', () => {
|
||||
it('should create a new device token when it does not exist', async () => {
|
||||
const newToken = createMockDeviceToken({ pushToken: 'ExponentPushToken[new-token]' });
|
||||
// Check for existing token - not found
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
// Insert returning
|
||||
mockDb.returning.mockResolvedValueOnce([newToken]);
|
||||
|
||||
const result = await service.registerToken(TEST_USER_ID, {
|
||||
pushToken: 'ExponentPushToken[new-token]',
|
||||
platform: 'ios',
|
||||
deviceName: 'Test iPhone',
|
||||
});
|
||||
|
||||
expect(result).toEqual(newToken);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(mockDb.values).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update existing token when it already exists', async () => {
|
||||
const existingToken = createMockDeviceToken({
|
||||
pushToken: 'ExponentPushToken[existing-token]',
|
||||
});
|
||||
const updatedToken = { ...existingToken, userId: TEST_USER_ID, isActive: true };
|
||||
|
||||
// Check for existing token - found
|
||||
mockDb.where.mockResolvedValueOnce([existingToken]);
|
||||
// Update returning
|
||||
mockDb.returning.mockResolvedValueOnce([updatedToken]);
|
||||
|
||||
const result = await service.registerToken(TEST_USER_ID, {
|
||||
pushToken: 'ExponentPushToken[existing-token]',
|
||||
platform: 'ios',
|
||||
deviceName: 'Test iPhone',
|
||||
});
|
||||
|
||||
expect(result).toEqual(updatedToken);
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalled();
|
||||
// Should not have called insert
|
||||
expect(mockDb.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeToken', () => {
|
||||
it('should delete a device token', async () => {
|
||||
await service.removeToken('ExponentPushToken[test-token]');
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalled();
|
||||
expect(mockDb.where).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivateToken', () => {
|
||||
it('should set token isActive to false', async () => {
|
||||
await service.deactivateToken('ExponentPushToken[test-token]');
|
||||
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalledWith(expect.objectContaining({ isActive: false }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveTokensForUser', () => {
|
||||
it('should return all active tokens for a user', async () => {
|
||||
const tokens = [
|
||||
createMockDeviceToken({ isActive: true }),
|
||||
createMockDeviceToken({ isActive: true }),
|
||||
];
|
||||
mockDb.where.mockResolvedValueOnce(tokens);
|
||||
|
||||
const result = await service.getActiveTokensForUser(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(tokens);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should return empty array when user has no active tokens', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getActiveTokensForUser(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendToUser', () => {
|
||||
it('should return false when user has no active tokens', async () => {
|
||||
// getActiveTokensForUser returns empty
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.sendToUser(TEST_USER_ID, {
|
||||
title: 'Test',
|
||||
body: 'Test notification',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockPushService.sendToTokens).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send notification to all active tokens', async () => {
|
||||
const token1 = createMockDeviceToken({ pushToken: 'token-1' });
|
||||
const token2 = createMockDeviceToken({ pushToken: 'token-2' });
|
||||
|
||||
// getActiveTokensForUser returns tokens
|
||||
mockDb.where.mockResolvedValueOnce([token1, token2]);
|
||||
|
||||
const resultMap = new Map<string, boolean>();
|
||||
resultMap.set('token-1', true);
|
||||
resultMap.set('token-2', true);
|
||||
mockPushService.sendToTokens!.mockResolvedValueOnce(resultMap);
|
||||
|
||||
const result = await service.sendToUser(TEST_USER_ID, {
|
||||
title: 'Test',
|
||||
body: 'Test notification',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPushService.sendToTokens).toHaveBeenCalledWith(['token-1', 'token-2'], {
|
||||
title: 'Test',
|
||||
body: 'Test notification',
|
||||
});
|
||||
});
|
||||
|
||||
it('should deactivate tokens that failed', async () => {
|
||||
const token1 = createMockDeviceToken({ pushToken: 'token-1' });
|
||||
const token2 = createMockDeviceToken({ pushToken: 'token-2' });
|
||||
|
||||
// getActiveTokensForUser returns tokens
|
||||
mockDb.where.mockResolvedValueOnce([token1, token2]);
|
||||
|
||||
const resultMap = new Map<string, boolean>();
|
||||
resultMap.set('token-1', true);
|
||||
resultMap.set('token-2', false); // This token failed
|
||||
mockPushService.sendToTokens!.mockResolvedValueOnce(resultMap);
|
||||
|
||||
const result = await service.sendToUser(TEST_USER_ID, {
|
||||
title: 'Test',
|
||||
body: 'Test notification',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
// Should deactivate the failed token
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
expect(mockDb.set).toHaveBeenCalledWith(expect.objectContaining({ isActive: false }));
|
||||
});
|
||||
|
||||
it('should return false when all tokens fail', async () => {
|
||||
const token1 = createMockDeviceToken({ pushToken: 'token-1' });
|
||||
|
||||
mockDb.where.mockResolvedValueOnce([token1]);
|
||||
|
||||
const resultMap = new Map<string, boolean>();
|
||||
resultMap.set('token-1', false);
|
||||
mockPushService.sendToTokens!.mockResolvedValueOnce(resultMap);
|
||||
|
||||
const result = await service.sendToUser(TEST_USER_ID, {
|
||||
title: 'Test',
|
||||
body: 'Test notification',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTokenCount', () => {
|
||||
it('should return count of active tokens', async () => {
|
||||
const tokens = [
|
||||
createMockDeviceToken({ isActive: true }),
|
||||
createMockDeviceToken({ isActive: true }),
|
||||
createMockDeviceToken({ isActive: true }),
|
||||
];
|
||||
mockDb.where.mockResolvedValueOnce(tokens);
|
||||
|
||||
const result = await service.getTokenCount(TEST_USER_ID);
|
||||
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it('should return 0 when user has no active tokens', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getTokenCount(TEST_USER_ID);
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
357
apps/calendar/apps/backend/src/sync/sync.service.spec.ts
Normal file
357
apps/calendar/apps/backend/src/sync/sync.service.spec.ts
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { SyncService } from './sync.service';
|
||||
import { ICalService } from './ical.service';
|
||||
import { CalDavService } from './caldav.service';
|
||||
import { GoogleCalendarService } from './google-calendar.service';
|
||||
import { EncryptionService } from '../common/encryption.service';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { TEST_USER_ID } from '../__tests__/utils/mock-factories';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
function createMockExternalCalendar(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
userId: TEST_USER_ID,
|
||||
name: 'External Calendar',
|
||||
provider: 'ical_url',
|
||||
calendarUrl: 'https://example.com/calendar.ics',
|
||||
username: null,
|
||||
encryptedPassword: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
tokenExpiresAt: null,
|
||||
syncEnabled: true,
|
||||
syncDirection: 'both',
|
||||
syncInterval: 15,
|
||||
lastSyncAt: null,
|
||||
lastSyncError: null,
|
||||
color: '#6B7280',
|
||||
isVisible: true,
|
||||
providerData: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SyncService', () => {
|
||||
let service: SyncService;
|
||||
let mockDb: any;
|
||||
let mockICalService: any;
|
||||
let mockCalDavService: any;
|
||||
let mockGoogleCalendarService: any;
|
||||
let mockEncryptionService: any;
|
||||
|
||||
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().mockResolvedValue([]),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
mockICalService = {
|
||||
fetchAndParseICalUrl: jest.fn().mockResolvedValue([]),
|
||||
generateICalData: jest.fn().mockReturnValue('BEGIN:VCALENDAR\nEND:VCALENDAR'),
|
||||
};
|
||||
|
||||
mockCalDavService = {
|
||||
discoverCalendars: jest.fn().mockResolvedValue([]),
|
||||
getAppleCalDavUrl: jest.fn().mockReturnValue('https://caldav.icloud.com'),
|
||||
fetchEvents: jest.fn().mockResolvedValue({ events: [], ctag: null }),
|
||||
upsertEvent: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
mockGoogleCalendarService = {
|
||||
isConfigured: jest.fn().mockReturnValue(false),
|
||||
getAuthUrl: jest.fn().mockReturnValue('https://google.com/auth'),
|
||||
exchangeCodeForTokens: jest.fn(),
|
||||
listCalendars: jest.fn(),
|
||||
refreshAccessToken: jest.fn(),
|
||||
fetchEvents: jest.fn().mockResolvedValue([]),
|
||||
createEvent: jest.fn(),
|
||||
updateEvent: jest.fn(),
|
||||
};
|
||||
|
||||
mockEncryptionService = {
|
||||
encrypt: jest.fn().mockReturnValue('encrypted-password'),
|
||||
decrypt: jest.fn().mockReturnValue('decrypted-password'),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SyncService,
|
||||
{
|
||||
provide: DATABASE_CONNECTION,
|
||||
useValue: mockDb,
|
||||
},
|
||||
{
|
||||
provide: ICalService,
|
||||
useValue: mockICalService,
|
||||
},
|
||||
{
|
||||
provide: CalDavService,
|
||||
useValue: mockCalDavService,
|
||||
},
|
||||
{
|
||||
provide: GoogleCalendarService,
|
||||
useValue: mockGoogleCalendarService,
|
||||
},
|
||||
{
|
||||
provide: EncryptionService,
|
||||
useValue: mockEncryptionService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SyncService>(SyncService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findByUser', () => {
|
||||
it('should return all external calendars for a user', async () => {
|
||||
const calendars = [
|
||||
createMockExternalCalendar({ name: 'Calendar 1' }),
|
||||
createMockExternalCalendar({ name: 'Calendar 2' }),
|
||||
];
|
||||
mockDb.where.mockResolvedValueOnce(calendars);
|
||||
|
||||
const result = await service.findByUser(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(calendars);
|
||||
expect(mockDb.select).toHaveBeenCalled();
|
||||
expect(mockDb.from).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array when user has no external calendars', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.findByUser(TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should return an external calendar when found', async () => {
|
||||
const calendar = createMockExternalCalendar();
|
||||
mockDb.where.mockResolvedValueOnce([calendar]);
|
||||
|
||||
const result = await service.findOne(calendar.id as string, TEST_USER_ID);
|
||||
|
||||
expect(result).toEqual(calendar);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when calendar not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.findOne('non-existent-id', TEST_USER_ID)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect', () => {
|
||||
it('should connect an iCal URL calendar', async () => {
|
||||
const newCalendar = createMockExternalCalendar({ provider: 'ical_url' });
|
||||
mockDb.returning.mockResolvedValueOnce([newCalendar]);
|
||||
|
||||
const result = await service.connect(TEST_USER_ID, {
|
||||
name: 'External Calendar',
|
||||
provider: 'ical_url',
|
||||
calendarUrl: 'https://example.com/calendar.ics',
|
||||
});
|
||||
|
||||
expect(result).toEqual(newCalendar);
|
||||
expect(mockICalService.fetchAndParseICalUrl).toHaveBeenCalledWith(
|
||||
'https://example.com/calendar.ics'
|
||||
);
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for CalDAV without credentials', async () => {
|
||||
await expect(
|
||||
service.connect(TEST_USER_ID, {
|
||||
name: 'CalDAV Calendar',
|
||||
provider: 'caldav',
|
||||
calendarUrl: 'https://caldav.example.com/cal',
|
||||
})
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should connect a CalDAV calendar with credentials', async () => {
|
||||
const newCalendar = createMockExternalCalendar({ provider: 'caldav' });
|
||||
mockDb.returning.mockResolvedValueOnce([newCalendar]);
|
||||
|
||||
const result = await service.connect(TEST_USER_ID, {
|
||||
name: 'CalDAV Calendar',
|
||||
provider: 'caldav',
|
||||
calendarUrl: 'https://caldav.example.com/cal',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
});
|
||||
|
||||
expect(result).toEqual(newCalendar);
|
||||
expect(mockCalDavService.discoverCalendars).toHaveBeenCalled();
|
||||
expect(mockEncryptionService.encrypt).toHaveBeenCalledWith('pass');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for Google without access token', async () => {
|
||||
await expect(
|
||||
service.connect(TEST_USER_ID, {
|
||||
name: 'Google Calendar',
|
||||
provider: 'google',
|
||||
calendarUrl: 'https://google.com/calendar',
|
||||
})
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('should disconnect an external calendar and delete synced events', async () => {
|
||||
const calendar = createMockExternalCalendar();
|
||||
mockDb.where.mockResolvedValueOnce([calendar]);
|
||||
|
||||
await service.disconnect(calendar.id as string, TEST_USER_ID);
|
||||
|
||||
// Should delete synced events and the calendar
|
||||
expect(mockDb.delete).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when calendar not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.disconnect('non-existent-id', TEST_USER_ID)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update external calendar settings', async () => {
|
||||
const calendar = createMockExternalCalendar();
|
||||
const updatedCalendar = { ...calendar, name: 'Updated Name' };
|
||||
|
||||
// findOne
|
||||
mockDb.where.mockResolvedValueOnce([calendar]);
|
||||
// update returning
|
||||
mockDb.returning.mockResolvedValueOnce([updatedCalendar]);
|
||||
|
||||
const result = await service.update(calendar.id as string, TEST_USER_ID, {
|
||||
name: 'Updated Name',
|
||||
});
|
||||
|
||||
expect(result.name).toBe('Updated Name');
|
||||
expect(mockDb.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when updating non-existent calendar', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(
|
||||
service.update('non-existent-id', TEST_USER_ID, { name: 'New Name' })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncCalendar', () => {
|
||||
it('should return early when sync is disabled', async () => {
|
||||
const calendar = createMockExternalCalendar({ syncEnabled: false });
|
||||
mockDb.where.mockResolvedValueOnce([calendar]);
|
||||
|
||||
const result = await service.syncCalendar(calendar.id as string);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.eventsImported).toBe(0);
|
||||
expect(result.eventsExported).toBe(0);
|
||||
expect(result.errors).toContain('Sync is disabled');
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when calendar not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.syncCalendar('non-existent-id')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('discoverCalDav', () => {
|
||||
it('should discover CalDAV calendars', async () => {
|
||||
mockCalDavService.discoverCalendars!.mockResolvedValueOnce([
|
||||
{
|
||||
url: 'https://caldav.example.com/cal/1',
|
||||
displayName: 'My Calendar',
|
||||
color: '#3B82F6',
|
||||
description: 'Test calendar',
|
||||
ctag: null,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.discoverCalDav({
|
||||
serverUrl: 'https://caldav.example.com',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
});
|
||||
|
||||
expect(result.calendars).toHaveLength(1);
|
||||
expect(result.calendars[0].name).toBe('My Calendar');
|
||||
expect(result.calendars[0].url).toBe('https://caldav.example.com/cal/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGoogleAuthUrl', () => {
|
||||
it('should throw BadRequestException when Google is not configured', () => {
|
||||
mockGoogleCalendarService.isConfigured!.mockReturnValue(false);
|
||||
|
||||
expect(() => service.getGoogleAuthUrl()).toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should return auth URL when Google is configured', () => {
|
||||
mockGoogleCalendarService.isConfigured!.mockReturnValue(true);
|
||||
mockGoogleCalendarService.getAuthUrl!.mockReturnValue('https://google.com/auth?state=test');
|
||||
|
||||
const result = service.getGoogleAuthUrl('test');
|
||||
|
||||
expect(result).toBe('https://google.com/auth?state=test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportCalendarAsIcal', () => {
|
||||
it('should export a calendar as iCal data', async () => {
|
||||
const calendar = {
|
||||
id: uuidv4(),
|
||||
userId: TEST_USER_ID,
|
||||
name: 'My Calendar',
|
||||
};
|
||||
const calendarEvents = [{ id: uuidv4(), title: 'Event 1' }];
|
||||
|
||||
// Find calendar
|
||||
mockDb.where.mockResolvedValueOnce([calendar]);
|
||||
// Find events
|
||||
mockDb.where.mockResolvedValueOnce(calendarEvents);
|
||||
|
||||
const result = await service.exportCalendarAsIcal(calendar.id, TEST_USER_ID);
|
||||
|
||||
expect(result).toBe('BEGIN:VCALENDAR\nEND:VCALENDAR');
|
||||
expect(mockICalService.generateICalData).toHaveBeenCalledWith('My Calendar', calendarEvents);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when calendar not found', async () => {
|
||||
mockDb.where.mockResolvedValueOnce([]);
|
||||
|
||||
await expect(service.exportCalendarAsIcal('non-existent-id', TEST_USER_ID)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue