Files
back_end_oudelaa/src/modules/engagement/engagement.service.spec.ts
T

170 lines
6.0 KiB
TypeScript

import { EngagementEventType } from './engagement-event-type.enum';
import { EngagementService } from './engagement.service';
describe('EngagementService', () => {
const userId = '507f1f77bcf86cd799439011';
const postId = '507f191e810c19729de860ea';
const setup = () => {
const eventModel = {
create: jest.fn().mockResolvedValue({}),
aggregate: jest.fn(),
};
const preferenceModel = {
updateOne: jest.fn().mockResolvedValue({ acknowledged: true }),
deleteOne: jest.fn().mockResolvedValue({ acknowledged: true }),
find: jest.fn(),
};
const postsService = {
findById: jest.fn().mockResolvedValue({ id: postId }),
};
const feedVersionService = {
bumpUserVersion: jest.fn().mockResolvedValue(2),
};
const service = new EngagementService(
eventModel as any,
preferenceModel as any,
postsService as any,
feedVersionService as any,
);
return { service, eventModel, preferenceModel, postsService, feedVersionService };
};
it('records a normalized engagement event after checking post visibility', async () => {
const { service, eventModel, postsService } = setup();
await expect(
service.track(userId, {
postId,
type: EngagementEventType.WATCH,
watchTimeMs: 9000,
progressPercent: 72,
sessionId: ' session-1 ',
}),
).resolves.toEqual({ accepted: true, postId, type: EngagementEventType.WATCH });
expect(postsService.findById).toHaveBeenCalledWith(postId, userId);
expect(eventModel.create).toHaveBeenCalledWith(
expect.objectContaining({
userId: expect.objectContaining({}),
postId: expect.objectContaining({}),
type: EngagementEventType.WATCH,
watchTimeMs: 9000,
progressPercent: 72,
sessionId: 'session-1',
occurredAt: expect.any(Date),
}),
);
});
it('uses safe defaults for optional event measurements', async () => {
const { service, eventModel } = setup();
await service.track(userId, { postId, type: EngagementEventType.IMPRESSION });
expect(eventModel.create).toHaveBeenCalledWith(
expect.objectContaining({ watchTimeMs: 0, progressPercent: 0, sessionId: '' }),
);
});
it('routes not-interested events through the preference workflow', async () => {
const { service, eventModel, preferenceModel, feedVersionService } = setup();
await expect(
service.track(userId, { postId, type: EngagementEventType.NOT_INTERESTED }),
).resolves.toEqual({ postId, notInterested: true });
expect(preferenceModel.updateOne).toHaveBeenCalledWith(
expect.objectContaining({ userId: expect.anything(), postId: expect.anything() }),
{ $set: { notInterested: true } },
{ upsert: true },
);
expect(eventModel.create).toHaveBeenCalledWith(
expect.objectContaining({ type: EngagementEventType.NOT_INTERESTED }),
);
expect(feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
});
it('tracks a batch and reports every accepted item', async () => {
const { service } = setup();
const events = [
{ postId, type: EngagementEventType.OPEN },
{ postId, type: EngagementEventType.COMPLETE },
];
await expect(service.trackBatch(userId, events)).resolves.toMatchObject({
accepted: 2,
items: [
{ accepted: true, type: EngagementEventType.OPEN },
{ accepted: true, type: EngagementEventType.COMPLETE },
],
});
});
it('removes a not-interested preference without requiring post visibility', async () => {
const { service, preferenceModel, postsService, eventModel, feedVersionService } = setup();
await expect(service.setNotInterested(userId, postId, false)).resolves.toEqual({
postId,
notInterested: false,
});
expect(postsService.findById).not.toHaveBeenCalled();
expect(preferenceModel.deleteOne).toHaveBeenCalledWith(
expect.objectContaining({ userId: expect.anything(), postId: expect.anything() }),
);
expect(eventModel.create).not.toHaveBeenCalled();
expect(feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
});
it('returns stored not-interested post ids as strings', async () => {
const { service, preferenceModel } = setup();
const exec = jest.fn().mockResolvedValue([{ postId }]);
const lean = jest.fn(() => ({ exec }));
const select = jest.fn(() => ({ lean }));
preferenceModel.find.mockReturnValue({ select });
await expect(service.getNotInterestedPostIds(userId)).resolves.toEqual([postId]);
expect(preferenceModel.find).toHaveBeenCalledWith(
expect.objectContaining({ userId: expect.anything(), notInterested: true }),
);
expect(select).toHaveBeenCalledWith({ postId: 1 });
});
it('returns ranking aggregates keyed by post id', async () => {
const { service, eventModel } = setup();
const row = { _id: { toString: () => postId }, watchMs: 5000, completions: 1, rewatches: 2, profileOpens: 1 };
const exec = jest.fn().mockResolvedValue([row]);
eventModel.aggregate.mockReturnValue({ exec });
const result = await service.getRankingSignals(userId);
expect(result).toEqual(new Map([[postId, row]]));
expect(eventModel.aggregate).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ $match: expect.objectContaining({ userId: expect.anything() }) }),
{ $sort: { watchMs: -1 } },
{ $limit: 500 },
]),
);
});
it('builds a 30-day engagement summary grouped by type', async () => {
const { service, eventModel } = setup();
eventModel.aggregate.mockReturnValue({
exec: jest.fn().mockResolvedValue([
{ _id: EngagementEventType.WATCH, count: 3, watchTimeMs: 12000 },
{ _id: EngagementEventType.SHARE, count: 1, watchTimeMs: 0 },
]),
});
await expect(service.getMySummary(userId)).resolves.toEqual({
periodDays: 30,
byType: {
watch: { count: 3, watchTimeMs: 12000 },
share: { count: 1, watchTimeMs: 0 },
},
});
});
});