Files
outline/server/queues/tasks/UpdateDocumentsPopularityScoreTask.test.ts
T
Tom Moor 091346dfe8 chore: Migrate to vitest (#12272)
* wip

* Remove obsolete snapshots

* simplify

* chore(test): Convert mocks to TypeScript and tighten fetch mock types

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Remove unneccessary patches

* Migrate to msw instead of custom fetch mock

* Address PR review comments

- Split chained vi.useFakeTimers().setSystemTime() into separate calls.
- Switch test setup to dynamic imports so EventEmitter.defaultMaxListeners
  assignment runs before module init (static imports were hoisted above it).
- Drop redundant NODE_ENV guard in monkeyPatchSequelizeErrorsForJest; its
  sole caller already gates on env.isTest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 21:10:51 -04:00

199 lines
5.3 KiB
TypeScript

import { subDays } from "date-fns";
import { Document, Revision, View } from "@server/models";
import { sequelize, sequelizeReadOnly } from "@server/storage/database";
import {
buildDocument,
buildTeam,
buildUser,
buildComment,
} from "@server/test/factories";
import UpdateDocumentsPopularityScoreTask from "./UpdateDocumentsPopularityScoreTask";
const props = {
limit: 10000,
partition: {
partitionIndex: 0,
partitionCount: 1,
},
};
vi.setConfig({ testTimeout: 30000 });
describe("UpdateDocumentsPopularityScoreTask", () => {
let task: UpdateDocumentsPopularityScoreTask;
beforeEach(() => {
task = new UpdateDocumentsPopularityScoreTask();
vi.spyOn(Date.prototype, "getHours").mockReturnValue(0);
// Ensure calculation query sees data created in tests by redirecting to main sequelize instance.
// We only mock if the instances are different to avoid infinite recursion.
if (sequelizeReadOnly !== sequelize) {
vi.spyOn(sequelizeReadOnly, "query").mockImplementation(
sequelize.query.bind(sequelize)
);
}
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should skip execution if not at a 6-hour interval", async () => {
vi.spyOn(Date.prototype, "getHours").mockReturnValue(1);
const team = await buildTeam();
const document = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
});
await task.perform(props);
const updatedDocument = await Document.findByPk(document.id);
expect(updatedDocument?.popularityScore).toBe(0);
});
it("should update popularity score based on revisions", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
});
await Revision.create({
documentId: document.id,
userId: user.id,
title: document.title,
text: "Content",
createdAt: subDays(new Date(), 1),
});
await task.perform(props);
const updatedDocument = await Document.findByPk(document.id);
expect(updatedDocument?.popularityScore).toBeGreaterThan(0);
});
it("should update popularity score based on views", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
});
await View.create({
documentId: document.id,
userId: user.id,
});
await task.perform(props);
const updatedDocument = await Document.findByPk(document.id);
expect(updatedDocument?.popularityScore).toBeGreaterThan(0);
});
it("should update popularity score based on comments", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const document = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
});
await buildComment({
documentId: document.id,
userId: user.id,
});
await task.perform(props);
const updatedDocument = await Document.findByPk(document.id);
expect(updatedDocument?.popularityScore).toBeGreaterThan(0);
});
it("should handle multiple documents and give higher score to more recent activity", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
const doc1 = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
});
const doc2 = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
});
// Recent activity for doc1
await Revision.create({
documentId: doc1.id,
userId: user.id,
title: doc1.title,
text: "Content",
createdAt: subDays(new Date(), 1),
});
// Older activity for doc2
await Revision.create({
documentId: doc2.id,
userId: user.id,
title: doc2.title,
text: "Content",
createdAt: subDays(new Date(), 5),
});
await task.perform(props);
const updatedDoc1 = await Document.findByPk(doc1.id);
const updatedDoc2 = await Document.findByPk(doc2.id);
expect(updatedDoc1?.popularityScore).toBeGreaterThan(
updatedDoc2?.popularityScore || 0
);
});
it("should only process published and non-deleted documents", async () => {
const team = await buildTeam();
const user = await buildUser({ teamId: team.id });
// Unpublished document (draft)
const draft = await buildDocument({
teamId: team.id,
publishedAt: undefined,
});
await Revision.create({
documentId: draft.id,
userId: user.id,
title: draft.title,
text: "Content",
createdAt: new Date(),
});
// Deleted document
const deleted = await buildDocument({
teamId: team.id,
publishedAt: new Date(),
deletedAt: new Date(),
});
await Revision.create({
documentId: deleted.id,
userId: user.id,
title: deleted.title,
text: "Content",
createdAt: new Date(),
});
await task.perform(props);
const updatedDraft = await Document.unscoped().findByPk(draft.id);
const updatedDeleted = await Document.unscoped().findByPk(deleted.id, {
paranoid: false,
});
expect(Number(updatedDraft?.popularityScore)).toEqual(0);
expect(Number(updatedDeleted?.popularityScore)).toEqual(0);
});
});