170 lines
6.0 KiB
TypeScript
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 },
|
|
},
|
|
});
|
|
});
|
|
});
|