Skip to content

Commit 42287da

Browse files
committed
chore: add tests
1 parent ec4350a commit 42287da

2 files changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import '../../src/env-test';
2+
import { ObjectId } from 'mongodb';
3+
4+
const collectionMock = {
5+
find: jest.fn(),
6+
bulkWrite: jest.fn(),
7+
};
8+
9+
jest.mock('../../src/redisHelper', () => ({
10+
__esModule: true,
11+
default: {
12+
getInstance: () => ({}),
13+
},
14+
}));
15+
16+
jest.mock('../../src/services/chartDataService', () => ({
17+
__esModule: true,
18+
default: jest.fn().mockImplementation(function () {
19+
return {};
20+
}),
21+
}));
22+
23+
jest.mock('../../src/dataLoaders', () => ({
24+
createProjectEventsByIdLoader: () => ({}),
25+
}));
26+
27+
jest.mock('../../src/mongo', () => ({
28+
databases: {
29+
events: {
30+
collection: jest.fn(() => collectionMock),
31+
},
32+
},
33+
}));
34+
35+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-explicit-any -- CJS class
36+
const EventsFactory = require('../../src/models/eventsFactory') as any;
37+
38+
describe('EventsFactory.bulkToggleEventMark', () => {
39+
const projectId = '507f1f77bcf86cd799439011';
40+
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
collectionMock.bulkWrite.mockResolvedValue({
44+
modifiedCount: 0,
45+
upsertedCount: 0,
46+
insertedCount: 0,
47+
matchedCount: 0,
48+
deletedCount: 0,
49+
});
50+
});
51+
52+
it('should throw when mark is not resolved or ignored', async () => {
53+
const factory = new EventsFactory(projectId);
54+
55+
await expect(factory.bulkToggleEventMark([], 'starred' as any)).rejects.toThrow(
56+
'bulkToggleEventMark: mark must be resolved or ignored'
57+
);
58+
});
59+
60+
it('should reject more than BULK_TOGGLE_EVENT_MARK_MAX unique ids', async () => {
61+
const factory = new EventsFactory(projectId);
62+
const max = EventsFactory.BULK_TOGGLE_EVENT_MARK_MAX;
63+
const ids = Array.from({ length: max + 1 }, (_, i) => `id-${i}`);
64+
65+
await expect(factory.bulkToggleEventMark(ids, 'ignored')).rejects.toThrow(
66+
`bulkToggleEventMark: at most ${max} event ids allowed`
67+
);
68+
});
69+
70+
it('should deduplicate duplicate event ids before applying', async () => {
71+
const factory = new EventsFactory(projectId);
72+
const id = new ObjectId();
73+
74+
collectionMock.find.mockReturnValue({
75+
toArray: () =>
76+
Promise.resolve([
77+
{
78+
_id: id,
79+
marks: {},
80+
},
81+
]),
82+
});
83+
collectionMock.bulkWrite.mockResolvedValue({
84+
modifiedCount: 1,
85+
upsertedCount: 0,
86+
});
87+
88+
await factory.bulkToggleEventMark([ id.toString(), id.toString(), id.toString() ], 'ignored');
89+
90+
expect(collectionMock.bulkWrite).toHaveBeenCalledTimes(1);
91+
const ops = collectionMock.bulkWrite.mock.calls[0][0];
92+
93+
expect(ops).toHaveLength(1);
94+
});
95+
96+
it('should return failedEventIds for invalid ObjectIds and skip bulkWrite', async () => {
97+
const factory = new EventsFactory(projectId);
98+
99+
const result = await factory.bulkToggleEventMark([ 'not-a-valid-id' ], 'resolved');
100+
101+
expect(result.updatedCount).toBe(0);
102+
expect(result.failedEventIds).toContain('not-a-valid-id');
103+
expect(collectionMock.bulkWrite).not.toHaveBeenCalled();
104+
});
105+
106+
it('should list valid but missing document ids in failedEventIds', async () => {
107+
const factory = new EventsFactory(projectId);
108+
const missing = new ObjectId();
109+
110+
collectionMock.find.mockReturnValue({
111+
toArray: () => Promise.resolve([]),
112+
});
113+
114+
const result = await factory.bulkToggleEventMark([ missing.toString() ], 'ignored');
115+
116+
expect(result.updatedCount).toBe(0);
117+
expect(result.failedEventIds).toContain(missing.toString());
118+
expect(collectionMock.bulkWrite).not.toHaveBeenCalled();
119+
});
120+
121+
it('should set mark only on events that do not have it when selection is mixed', async () => {
122+
const factory = new EventsFactory(projectId);
123+
const a = new ObjectId();
124+
const b = new ObjectId();
125+
126+
collectionMock.find.mockReturnValue({
127+
toArray: () =>
128+
Promise.resolve([
129+
{ _id: a, marks: { ignored: 1 } },
130+
{ _id: b, marks: {} },
131+
]),
132+
});
133+
collectionMock.bulkWrite.mockResolvedValue({
134+
modifiedCount: 1,
135+
upsertedCount: 0,
136+
});
137+
138+
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored');
139+
140+
expect(result.updatedCount).toBe(1);
141+
const ops = collectionMock.bulkWrite.mock.calls[0][0];
142+
143+
expect(ops).toHaveLength(1);
144+
expect(ops[0].updateOne.filter._id).toEqual(b);
145+
expect(ops[0].updateOne.update).toEqual(
146+
expect.objectContaining({
147+
$set: { 'marks.ignored': expect.any(Number) },
148+
})
149+
);
150+
});
151+
152+
it('should remove mark from all when every selected event already has the mark', async () => {
153+
const factory = new EventsFactory(projectId);
154+
const a = new ObjectId();
155+
const b = new ObjectId();
156+
157+
collectionMock.find.mockReturnValue({
158+
toArray: () =>
159+
Promise.resolve([
160+
{ _id: a, marks: { resolved: 1 } },
161+
{ _id: b, marks: { resolved: 2 } },
162+
]),
163+
});
164+
collectionMock.bulkWrite.mockResolvedValue({
165+
modifiedCount: 2,
166+
upsertedCount: 0,
167+
});
168+
169+
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'resolved');
170+
171+
expect(result.updatedCount).toBe(2);
172+
const ops = collectionMock.bulkWrite.mock.calls[0][0];
173+
174+
expect(ops).toHaveLength(2);
175+
expect(ops[0].updateOne.update).toEqual({ $unset: { 'marks.resolved': '' } });
176+
expect(ops[1].updateOne.update).toEqual({ $unset: { 'marks.resolved': '' } });
177+
});
178+
179+
it('should not remove mark from a subset when only some of the found events have the mark', async () => {
180+
const factory = new EventsFactory(projectId);
181+
const a = new ObjectId();
182+
const b = new ObjectId();
183+
184+
collectionMock.find.mockReturnValue({
185+
toArray: () =>
186+
Promise.resolve([
187+
{ _id: a, marks: { ignored: 1 } },
188+
{ _id: b, marks: {} },
189+
]),
190+
});
191+
collectionMock.bulkWrite.mockResolvedValue({
192+
modifiedCount: 1,
193+
upsertedCount: 0,
194+
});
195+
196+
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored');
197+
198+
expect(result.updatedCount).toBe(1);
199+
const ops = collectionMock.bulkWrite.mock.calls[0][0];
200+
201+
expect(ops).toHaveLength(1);
202+
expect(ops[0].updateOne.update).toEqual(
203+
expect.objectContaining({ $set: { 'marks.ignored': expect.any(Number) } })
204+
);
205+
});
206+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import '../../src/env-test';
2+
3+
import { UserInputError } from 'apollo-server-express';
4+
5+
jest.mock('../../src/resolvers/helpers/eventsFactory', () => ({
6+
__esModule: true,
7+
default: jest.fn(),
8+
}));
9+
10+
import getEventsFactory from '../../src/resolvers/helpers/eventsFactory';
11+
// eslint-disable-next-line @typescript-eslint/no-var-requires
12+
const eventResolvers = require('../../src/resolvers/event') as {
13+
Mutation: {
14+
bulkToggleEventMarks: (
15+
o: unknown,
16+
args: { projectId: string; eventIds: string[]; mark: string },
17+
ctx: unknown
18+
) => Promise<{ updatedCount: number; failedEventIds: string[] }>;
19+
};
20+
};
21+
22+
const bulkToggleEventMark = jest.fn();
23+
24+
describe('Mutation.bulkToggleEventMarks', () => {
25+
const ctx = {};
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
(getEventsFactory as unknown as jest.Mock).mockReturnValue({ bulkToggleEventMark });
30+
});
31+
32+
it('should throw when mark is not resolved or ignored', async () => {
33+
await expect(
34+
eventResolvers.Mutation.bulkToggleEventMarks(
35+
{},
36+
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'starred' },
37+
ctx
38+
)
39+
).rejects.toThrow(UserInputError);
40+
41+
await expect(
42+
eventResolvers.Mutation.bulkToggleEventMarks(
43+
{},
44+
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'starred' },
45+
ctx
46+
)
47+
).rejects.toThrow('bulkToggleEventMarks supports only resolved and ignored marks');
48+
49+
expect(bulkToggleEventMark).not.toHaveBeenCalled();
50+
});
51+
52+
it('should throw when eventIds is empty', async () => {
53+
await expect(
54+
eventResolvers.Mutation.bulkToggleEventMarks(
55+
{},
56+
{ projectId: 'p1', eventIds: [], mark: 'ignored' },
57+
ctx
58+
)
59+
).rejects.toThrow('eventIds must contain at least one id');
60+
61+
expect(bulkToggleEventMark).not.toHaveBeenCalled();
62+
});
63+
64+
it('should call factory with original event ids and return its result', async () => {
65+
const payload = { updatedCount: 2, failedEventIds: [ 'x' ] };
66+
67+
bulkToggleEventMark.mockResolvedValue(payload);
68+
69+
const result = await eventResolvers.Mutation.bulkToggleEventMarks(
70+
{},
71+
{
72+
projectId: 'p1',
73+
eventIds: [ '507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012' ],
74+
mark: 'resolved',
75+
},
76+
ctx
77+
);
78+
79+
expect(getEventsFactory).toHaveBeenCalledWith(ctx, 'p1');
80+
expect(bulkToggleEventMark).toHaveBeenCalledWith(
81+
[ '507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012' ],
82+
'resolved'
83+
);
84+
expect(result).toEqual(payload);
85+
});
86+
87+
it('should map factory max-length error to UserInputError', async () => {
88+
bulkToggleEventMark.mockRejectedValue(
89+
new Error('bulkToggleEventMark: at most 100 event ids allowed')
90+
);
91+
92+
await expect(
93+
eventResolvers.Mutation.bulkToggleEventMarks(
94+
{},
95+
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439011' ], mark: 'ignored' },
96+
ctx
97+
)
98+
).rejects.toThrow(UserInputError);
99+
});
100+
});

0 commit comments

Comments
 (0)