Files
sendou.ink/app/features/notifications/NotificationRepository.server.test.ts
2026-08-15 16:38:46 +03:00

361 lines
10 KiB
TypeScript

import { beforeEach, describe, expect, test } from "vitest";
import * as NotificationFactory from "~/db/seed/factories/NotificationFactory";
import * as UserFactory from "~/db/seed/factories/UserFactory";
import { withUserId } from "~/utils/Test";
import * as NotificationRepository from "./NotificationRepository.server";
const users = UserFactory.pool();
const seenStatusOf = async (userId: number) => {
const notifications = await NotificationRepository.findByUserId(userId);
return notifications.map(({ type, seen }) => ({ type, seen }));
};
describe("markAsSeenByType", () => {
beforeEach(async () => {
await users.create(3);
});
test("marks unseen notification of the type as seen", async () => {
await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }],
});
await NotificationRepository.markAsSeenByType({
userIds: [users.id(1)],
type: "SQ_READY_CHECK",
});
expect(await seenStatusOf(users.id(1))).toEqual([
{ type: "SQ_READY_CHECK", seen: 1 },
]);
});
test("leaves other users' notifications unseen", async () => {
await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }, { userId: users.id(2) }],
});
await NotificationRepository.markAsSeenByType({
userIds: [users.id(1)],
type: "SQ_READY_CHECK",
});
expect(await seenStatusOf(users.id(2))).toEqual([
{ type: "SQ_READY_CHECK", seen: 0 },
]);
});
test("leaves notifications of other types unseen", async () => {
await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }],
});
await NotificationFactory.create({
notification: { type: "SQ_NEW_MATCH", meta: { matchId: 1 } },
users: [{ userId: users.id(1) }],
});
await NotificationRepository.markAsSeenByType({
userIds: [users.id(1)],
type: "SQ_READY_CHECK",
});
expect(await seenStatusOf(users.id(1))).toEqual([
{ type: "SQ_NEW_MATCH", seen: 0 },
{ type: "SQ_READY_CHECK", seen: 1 },
]);
});
test("meta filter only matches notifications with the given values", async () => {
await NotificationFactory.create({
notification: { type: "SQ_NEW_MATCH", meta: { matchId: 1 } },
users: [{ userId: users.id(1) }],
});
await NotificationFactory.create({
notification: { type: "SQ_NEW_MATCH", meta: { matchId: 2 } },
users: [{ userId: users.id(1) }],
});
await NotificationRepository.markAsSeenByType({
userIds: [users.id(1)],
type: "SQ_NEW_MATCH",
meta: { matchId: 2 },
});
expect(await seenStatusOf(users.id(1))).toEqual([
{ type: "SQ_NEW_MATCH", seen: 1 },
{ type: "SQ_NEW_MATCH", seen: 0 },
]);
});
test("matches on a subset of meta keys, both string and number valued", async () => {
await NotificationFactory.create({
notification: {
type: "SCRIM_NEW_REQUEST",
meta: { fromUserId: 1, fromUsername: "alice", scrimPostId: 7 },
},
users: [{ userId: users.id(1) }],
});
await NotificationFactory.create({
notification: {
type: "SCRIM_NEW_REQUEST",
meta: { fromUserId: 2, fromUsername: "bob", scrimPostId: 7 },
},
users: [{ userId: users.id(1) }],
});
await NotificationRepository.markAsSeenByType({
userIds: [users.id(1)],
type: "SCRIM_NEW_REQUEST",
meta: { fromUsername: "alice" },
});
expect(await seenStatusOf(users.id(1))).toEqual([
{ type: "SCRIM_NEW_REQUEST", seen: 0 },
{ type: "SCRIM_NEW_REQUEST", seen: 1 },
]);
});
test("marks the notification as seen for every given user", async () => {
await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [
{ userId: users.id(1) },
{ userId: users.id(2) },
{ userId: users.id(3) },
],
});
await NotificationRepository.markAsSeenByType({
userIds: [users.id(1), users.id(3)],
type: "SQ_READY_CHECK",
});
expect(await seenStatusOf(users.id(1))).toEqual([
{ type: "SQ_READY_CHECK", seen: 1 },
]);
expect(await seenStatusOf(users.id(2))).toEqual([
{ type: "SQ_READY_CHECK", seen: 0 },
]);
expect(await seenStatusOf(users.id(3))).toEqual([
{ type: "SQ_READY_CHECK", seen: 1 },
]);
});
});
describe("markOwnAsSeen", () => {
beforeEach(async () => {
await users.create(2);
});
test("returns the actor's id when a notification flips to seen", async () => {
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }],
});
const changedUserIds = await withUserId(users.id(1), () =>
NotificationRepository.markOwnAsSeen([notification.id]),
);
expect(changedUserIds).toEqual([users.id(1)]);
expect(await seenStatusOf(users.id(1))).toEqual([
{ type: "SQ_READY_CHECK", seen: 1 },
]);
});
test("returns no user ids when the notifications were already seen", async () => {
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1), seen: 1 }],
});
const changedUserIds = await withUserId(users.id(1), () =>
NotificationRepository.markOwnAsSeen([notification.id]),
);
expect(changedUserIds).toEqual([]);
});
test("returns the actor's id once even if many notifications flip", async () => {
const notifications = await Promise.all([
NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }],
}),
NotificationFactory.create({
notification: { type: "SQ_NEW_MATCH", meta: { matchId: 1 } },
users: [{ userId: users.id(1) }],
}),
]);
const changedUserIds = await withUserId(users.id(1), () =>
NotificationRepository.markOwnAsSeen(notifications.map(({ id }) => id)),
);
expect(changedUserIds).toEqual([users.id(1)]);
});
test("leaves another user's copy of the notification unseen", async () => {
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }, { userId: users.id(2) }],
});
await withUserId(users.id(1), () =>
NotificationRepository.markOwnAsSeen([notification.id]),
);
expect(await seenStatusOf(users.id(2))).toEqual([
{ type: "SQ_READY_CHECK", seen: 0 },
]);
});
});
describe("upsertOwnSubscription", () => {
beforeEach(async () => {
await users.create(2);
});
const subscription = (endpoint: string) => ({
endpoint,
keys: { auth: "auth", p256dh: "p256dh" },
});
test("resubscribing with the same endpoint does not duplicate the subscription", async () => {
await withUserId(users.id(1), () =>
NotificationRepository.upsertOwnSubscription(
subscription("https://push.example.com/1"),
),
);
await withUserId(users.id(1), () =>
NotificationRepository.upsertOwnSubscription(
subscription("https://push.example.com/1"),
),
);
const subscriptions =
await NotificationRepository.findAllSubscriptionsByUserIds([users.id(1)]);
expect(subscriptions).toHaveLength(1);
});
test("another user subscribing on the same browser takes over the endpoint", async () => {
await withUserId(users.id(1), () =>
NotificationRepository.upsertOwnSubscription(
subscription("https://push.example.com/1"),
),
);
await withUserId(users.id(2), () =>
NotificationRepository.upsertOwnSubscription(
subscription("https://push.example.com/1"),
),
);
expect(
await NotificationRepository.findAllSubscriptionsByUserIds([users.id(1)]),
).toHaveLength(0);
expect(
await NotificationRepository.findAllSubscriptionsByUserIds([users.id(2)]),
).toHaveLength(1);
});
test("different endpoints subscribe separately for the same user", async () => {
await withUserId(users.id(1), () =>
NotificationRepository.upsertOwnSubscription(
subscription("https://push.example.com/1"),
),
);
await withUserId(users.id(1), () =>
NotificationRepository.upsertOwnSubscription(
subscription("https://push.example.com/2"),
),
);
expect(
await NotificationRepository.findAllSubscriptionsByUserIds([users.id(1)]),
).toHaveLength(2);
});
});
describe("findUnseenSubscriptionsByNotificationId", () => {
beforeEach(async () => {
await users.create(3);
});
const subscribe = (userId: number, endpoint: string) =>
withUserId(userId, () =>
NotificationRepository.upsertOwnSubscription({
endpoint,
keys: { auth: "auth", p256dh: "p256dh" },
}),
);
const subscribedEndpointsOf = async (notificationId: number) => {
const subscriptions =
await NotificationRepository.findUnseenSubscriptionsByNotificationId(
notificationId,
);
return subscriptions.map(({ subscription }) => subscription.endpoint);
};
test("returns the subscriptions of recipients who have not seen the notification", async () => {
await subscribe(users.id(1), "https://push.example.com/1");
await subscribe(users.id(2), "https://push.example.com/2");
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }, { userId: users.id(2) }],
});
expect(await subscribedEndpointsOf(notification.id)).toEqual(
expect.arrayContaining([
"https://push.example.com/1",
"https://push.example.com/2",
]),
);
});
test("excludes recipients who have seen the notification", async () => {
await subscribe(users.id(1), "https://push.example.com/1");
await subscribe(users.id(2), "https://push.example.com/2");
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1), seen: 1 }, { userId: users.id(2) }],
});
expect(await subscribedEndpointsOf(notification.id)).toEqual([
"https://push.example.com/2",
]);
});
test("excludes subscriptions of users who did not receive the notification", async () => {
await subscribe(users.id(1), "https://push.example.com/1");
await subscribe(users.id(3), "https://push.example.com/3");
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }, { userId: users.id(2) }],
});
expect(await subscribedEndpointsOf(notification.id)).toEqual([
"https://push.example.com/1",
]);
});
test("returns every subscription of an unseen recipient", async () => {
await subscribe(users.id(1), "https://push.example.com/1");
await subscribe(users.id(1), "https://push.example.com/2");
const notification = await NotificationFactory.create({
notification: { type: "SQ_READY_CHECK" },
users: [{ userId: users.id(1) }],
});
expect(await subscribedEndpointsOf(notification.id)).toHaveLength(2);
});
});