mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
7e2d2542b0
* Initial plan * Split group notifications into separate background task - Created GroupMentionedInCommentNotificationsTask for handling group mention notifications - Updated CommentCreatedNotificationsTask to delegate group notifications - Updated CommentUpdatedNotificationsTask to delegate group notifications - Added comprehensive tests for the new task - Improved scalability with batch processing (10 members per batch) - Enhanced resilience by isolating group notification failures Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Optimize database queries in GroupMentionedInCommentNotificationsTask - Batch fetch users instead of individual queries for better performance - Add defensive comment explaining duplicate mentions check - Create user map for O(1) lookups within batch Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Add comment explaining concurrency control in batch processing Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Remove unnecessary document loading - use event properties directly - Removed Document import and document loading query - Use event.documentId and event.teamId directly - Reduces database queries and improves performance - canUserAccessDocument loads document internally when needed Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * Fix TypeScript errors and test failures - Fixed test: buildComment requires documentId parameter - Fixed type error: use nullish coalescing for group.actorId ?? event.actorId - Fixed type definition: use Omit<CommentEvent, "data"> to avoid type conflicts - All TypeScript errors resolved Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * test --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com>
314 lines
7.8 KiB
TypeScript
314 lines
7.8 KiB
TypeScript
import { NotificationEventType } from "@shared/types";
|
|
import { Notification } from "@server/models";
|
|
import {
|
|
buildUser,
|
|
buildDocument,
|
|
buildGroup,
|
|
buildGroupUser,
|
|
buildComment,
|
|
} from "@server/test/factories";
|
|
import GroupMentionedInCommentNotificationsTask from "./GroupMentionedInCommentNotificationsTask";
|
|
|
|
const ip = "127.0.0.1";
|
|
|
|
beforeEach(async () => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
describe("GroupMentionedInCommentNotificationsTask", () => {
|
|
it("should send notifications to all group members with access", async () => {
|
|
const spy = jest.spyOn(Notification, "create");
|
|
const actor = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: actor.teamId,
|
|
});
|
|
const comment = await buildComment({
|
|
userId: actor.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: actor.teamId,
|
|
});
|
|
|
|
const member1 = await buildUser({ teamId: actor.teamId });
|
|
const member2 = await buildUser({ teamId: actor.teamId });
|
|
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: member1.id,
|
|
});
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: member2.id,
|
|
});
|
|
|
|
member1.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment
|
|
);
|
|
await member1.save();
|
|
|
|
member2.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment
|
|
);
|
|
await member2.save();
|
|
|
|
const task = new GroupMentionedInCommentNotificationsTask();
|
|
await task.perform({
|
|
name: "comments.create",
|
|
modelId: comment.id,
|
|
documentId: document.id,
|
|
teamId: actor.teamId,
|
|
actorId: actor.id,
|
|
ip,
|
|
data: {
|
|
groupId: group.id,
|
|
actorId: actor.id,
|
|
},
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
expect(spy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
event: NotificationEventType.GroupMentionedInComment,
|
|
groupId: group.id,
|
|
userId: member1.id,
|
|
actorId: actor.id,
|
|
documentId: document.id,
|
|
commentId: comment.id,
|
|
})
|
|
);
|
|
expect(spy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
event: NotificationEventType.GroupMentionedInComment,
|
|
groupId: group.id,
|
|
userId: member2.id,
|
|
actorId: actor.id,
|
|
documentId: document.id,
|
|
commentId: comment.id,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should not send notification to actor", async () => {
|
|
const spy = jest.spyOn(Notification, "create");
|
|
const actor = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: actor.teamId,
|
|
});
|
|
const comment = await buildComment({
|
|
userId: actor.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: actor.teamId,
|
|
});
|
|
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: actor.id,
|
|
});
|
|
|
|
actor.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment
|
|
);
|
|
await actor.save();
|
|
|
|
const task = new GroupMentionedInCommentNotificationsTask();
|
|
await task.perform({
|
|
name: "comments.create",
|
|
modelId: comment.id,
|
|
documentId: document.id,
|
|
teamId: actor.teamId,
|
|
actorId: actor.id,
|
|
ip,
|
|
data: {
|
|
groupId: group.id,
|
|
actorId: actor.id,
|
|
},
|
|
});
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should not send notification if group has mentions disabled", async () => {
|
|
const spy = jest.spyOn(Notification, "create");
|
|
const actor = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: actor.teamId,
|
|
});
|
|
const comment = await buildComment({
|
|
userId: actor.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: actor.teamId,
|
|
disableMentions: true,
|
|
});
|
|
|
|
const member = await buildUser({ teamId: actor.teamId });
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: member.id,
|
|
});
|
|
|
|
member.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment
|
|
);
|
|
await member.save();
|
|
|
|
const task = new GroupMentionedInCommentNotificationsTask();
|
|
await task.perform({
|
|
name: "comments.create",
|
|
modelId: comment.id,
|
|
documentId: document.id,
|
|
teamId: actor.teamId,
|
|
actorId: actor.id,
|
|
ip,
|
|
data: {
|
|
groupId: group.id,
|
|
actorId: actor.id,
|
|
},
|
|
});
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should not send notification to users without subscription", async () => {
|
|
const spy = jest.spyOn(Notification, "create");
|
|
const actor = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: actor.teamId,
|
|
});
|
|
const comment = await buildComment({
|
|
userId: actor.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: actor.teamId,
|
|
});
|
|
|
|
const member = await buildUser({ teamId: actor.teamId });
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: member.id,
|
|
});
|
|
|
|
// member doesn't have notification subscription enabled
|
|
member.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment,
|
|
false
|
|
);
|
|
await member.save();
|
|
|
|
const task = new GroupMentionedInCommentNotificationsTask();
|
|
await task.perform({
|
|
name: "comments.create",
|
|
modelId: comment.id,
|
|
documentId: document.id,
|
|
teamId: actor.teamId,
|
|
actorId: actor.id,
|
|
ip,
|
|
data: {
|
|
groupId: group.id,
|
|
actorId: actor.id,
|
|
},
|
|
});
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should handle large groups with batching", async () => {
|
|
const spy = jest.spyOn(Notification, "create");
|
|
const actor = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: actor.teamId,
|
|
});
|
|
const comment = await buildComment({
|
|
userId: actor.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: actor.teamId,
|
|
});
|
|
|
|
// Create 25 members to test batching (batch size is 10)
|
|
const members = [];
|
|
for (let i = 0; i < 25; i++) {
|
|
const member = await buildUser({ teamId: actor.teamId });
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: member.id,
|
|
});
|
|
member.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment
|
|
);
|
|
await member.save();
|
|
members.push(member);
|
|
}
|
|
|
|
const task = new GroupMentionedInCommentNotificationsTask();
|
|
await task.perform({
|
|
name: "comments.create",
|
|
modelId: comment.id,
|
|
documentId: document.id,
|
|
teamId: actor.teamId,
|
|
actorId: actor.id,
|
|
ip,
|
|
data: {
|
|
groupId: group.id,
|
|
actorId: actor.id,
|
|
},
|
|
});
|
|
|
|
expect(spy).toHaveBeenCalledTimes(25);
|
|
});
|
|
|
|
it("should not send notification if document does not exist", async () => {
|
|
const spy = jest.spyOn(Notification, "create");
|
|
const actor = await buildUser();
|
|
const document = await buildDocument({
|
|
teamId: actor.teamId,
|
|
});
|
|
const comment = await buildComment({
|
|
userId: actor.id,
|
|
documentId: document.id,
|
|
});
|
|
|
|
const group = await buildGroup({
|
|
teamId: actor.teamId,
|
|
});
|
|
|
|
const member = await buildUser({ teamId: actor.teamId });
|
|
await buildGroupUser({
|
|
groupId: group.id,
|
|
userId: member.id,
|
|
});
|
|
|
|
member.setNotificationEventType(
|
|
NotificationEventType.GroupMentionedInComment
|
|
);
|
|
await member.save();
|
|
|
|
const task = new GroupMentionedInCommentNotificationsTask();
|
|
await task.perform({
|
|
name: "comments.create",
|
|
modelId: comment.id,
|
|
documentId: "non-existent-id",
|
|
teamId: actor.teamId,
|
|
actorId: actor.id,
|
|
ip,
|
|
data: {
|
|
groupId: group.id,
|
|
actorId: actor.id,
|
|
},
|
|
});
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
});
|