Integrate Oudelaa backend features, security, tests, and deployment updates
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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 },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user