mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-25 12:49:26 -05:00
386 lines
11 KiB
TypeScript
386 lines
11 KiB
TypeScript
import { addDays, subDays } from "date-fns";
|
|
import { beforeEach, describe, expect, test } from "vitest";
|
|
import * as ChatMessageFactory from "~/db/seed/factories/ChatMessageFactory";
|
|
import * as ChatRoomFactory from "~/db/seed/factories/ChatRoomFactory";
|
|
import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory";
|
|
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
|
import { db } from "~/db/sql";
|
|
import { invariant } from "~/utils/invariant";
|
|
import * as ChatRepository from "./ChatRepository.server";
|
|
|
|
const users = UserFactory.pool();
|
|
|
|
beforeEach(async () => {
|
|
await users.create(2);
|
|
});
|
|
|
|
describe("ChatRepository.insertMessage", () => {
|
|
test("returns the inserted row", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
|
|
const message = await ChatRepository.insertMessage({
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
contents: "hello",
|
|
publicId: "abc123",
|
|
});
|
|
|
|
expect(message.roomId).toBe(room.id);
|
|
expect(message.authorUserId).toBe(users.id(1));
|
|
expect(message.contents).toBe("hello");
|
|
expect(message.type).toBeNull();
|
|
});
|
|
|
|
test("retried insert with the same publicId returns the existing row without double-inserting", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
|
|
const first = await ChatRepository.insertMessage({
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
contents: "hello",
|
|
publicId: "abc123",
|
|
});
|
|
const retried = await ChatRepository.insertMessage({
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
contents: "hello again",
|
|
publicId: "abc123",
|
|
});
|
|
|
|
expect(retried.id).toBe(first.id);
|
|
expect(retried.contents).toBe("hello");
|
|
|
|
const count = await db
|
|
.selectFrom("ChatMessage")
|
|
.select(({ fn }) => fn.countAll<number>().as("count"))
|
|
.where("ChatMessage.roomId", "=", room.id)
|
|
.executeTakeFirstOrThrow();
|
|
expect(count.count).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.insertSystemMessage", () => {
|
|
test("inserts a typed message with no contents", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
|
|
const message = await ChatRepository.insertSystemMessage({
|
|
roomId: room.id,
|
|
type: "SCORE_REPORTED",
|
|
authorUserId: users.id(1),
|
|
});
|
|
|
|
expect(message.type).toBe("SCORE_REPORTED");
|
|
expect(message.contents).toBeNull();
|
|
expect(message.authorUserId).toBe(users.id(1));
|
|
expect(message.publicId).not.toBe("");
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.findAllMessagesByRoomId", () => {
|
|
test("returns messages oldest first with authors resolved", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
await ChatMessageFactory.create({
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
});
|
|
await ChatMessageFactory.create({
|
|
roomId: room.id,
|
|
authorUserId: users.id(2),
|
|
});
|
|
|
|
const messages = await ChatRepository.findAllMessagesByRoomId(room.id);
|
|
|
|
expect(messages).toHaveLength(2);
|
|
expect(messages[0].id).toBeLessThan(messages[1].id);
|
|
expect(messages[0].author?.id).toBe(users.id(1));
|
|
expect(messages[1].author?.id).toBe(users.id(2));
|
|
expect(messages[0].author?.username).toBeTruthy();
|
|
});
|
|
|
|
test("keeps the latest messages when over the limit", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
const [, second, third] = await ChatMessageFactory.createMany(3, {
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
});
|
|
|
|
const messages = await ChatRepository.findAllMessagesByRoomId(room.id, {
|
|
limit: 2,
|
|
});
|
|
|
|
expect(messages.map((message) => message.id)).toEqual([
|
|
second.id,
|
|
third.id,
|
|
]);
|
|
});
|
|
|
|
test("does not return another room's messages", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
const otherRoom = await ChatRoomFactory.create();
|
|
await ChatMessageFactory.create({
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
});
|
|
await ChatMessageFactory.create({
|
|
roomId: otherRoom.id,
|
|
authorUserId: users.id(1),
|
|
});
|
|
|
|
const messages = await ChatRepository.findAllMessagesByRoomId(room.id);
|
|
|
|
expect(messages).toHaveLength(1);
|
|
expect(messages[0].roomId).toBe(room.id);
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.findMessageById", () => {
|
|
test("returns the message with its author resolved", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
const inserted = await ChatMessageFactory.create({
|
|
roomId: room.id,
|
|
authorUserId: users.id(1),
|
|
});
|
|
|
|
const message = await ChatRepository.findMessageById(inserted.id);
|
|
|
|
expect(message?.roomId).toBe(room.id);
|
|
expect(message?.author?.id).toBe(users.id(1));
|
|
expect(message?.author?.username).toBeTruthy();
|
|
});
|
|
|
|
test("returns undefined for an unknown id", async () => {
|
|
expect(await ChatRepository.findMessageById(424242)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.findMessageStatsByRoomIds", () => {
|
|
test("counts messages newer than the user's read indicator per room", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
const otherRoom = await ChatRoomFactory.create();
|
|
const [first, , third] = await ChatMessageFactory.createMany(3, {
|
|
roomId: room.id,
|
|
authorUserId: users.id(2),
|
|
});
|
|
const otherRoomMessage = await ChatMessageFactory.create({
|
|
roomId: otherRoom.id,
|
|
authorUserId: users.id(2),
|
|
});
|
|
await ChatRepository.upsertReadIndicator({
|
|
userId: users.id(1),
|
|
roomId: room.id,
|
|
lastSeenMessageId: first.id,
|
|
});
|
|
|
|
const stats = await ChatRepository.findMessageStatsByRoomIds(users.id(1), [
|
|
room.id,
|
|
otherRoom.id,
|
|
]);
|
|
|
|
expect(stats.sort((a, b) => a.roomId - b.roomId)).toEqual([
|
|
{
|
|
roomId: room.id,
|
|
unreadCount: 2,
|
|
latestMessageId: third.id,
|
|
latestMessageCreatedAt: third.createdAt,
|
|
},
|
|
{
|
|
roomId: otherRoom.id,
|
|
unreadCount: 1,
|
|
latestMessageId: otherRoomMessage.id,
|
|
latestMessageCreatedAt: otherRoomMessage.createdAt,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("returns a zero unread count once everything is read", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
const message = await ChatMessageFactory.create({
|
|
roomId: room.id,
|
|
authorUserId: users.id(2),
|
|
});
|
|
await ChatRepository.upsertReadIndicator({
|
|
userId: users.id(1),
|
|
roomId: room.id,
|
|
lastSeenMessageId: message.id,
|
|
});
|
|
|
|
expect(
|
|
await ChatRepository.findMessageStatsByRoomIds(users.id(1), [room.id]),
|
|
).toEqual([
|
|
{
|
|
roomId: room.id,
|
|
unreadCount: 0,
|
|
latestMessageId: message.id,
|
|
latestMessageCreatedAt: message.createdAt,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("leaves out rooms with no messages", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
|
|
expect(
|
|
await ChatRepository.findMessageStatsByRoomIds(users.id(1), [room.id]),
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("returns an empty array for no room ids", async () => {
|
|
expect(
|
|
await ChatRepository.findMessageStatsByRoomIds(users.id(1), []),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.upsertReadIndicator", () => {
|
|
test("creates the indicator on first upsert", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
|
|
await ChatRepository.upsertReadIndicator({
|
|
userId: users.id(1),
|
|
roomId: room.id,
|
|
lastSeenMessageId: 5,
|
|
});
|
|
|
|
const indicator = await readIndicator(users.id(1), room.id);
|
|
expect(indicator.lastSeenMessageId).toBe(5);
|
|
});
|
|
|
|
test("never regresses to an older message", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
|
|
await ChatRepository.upsertReadIndicator({
|
|
userId: users.id(1),
|
|
roomId: room.id,
|
|
lastSeenMessageId: 5,
|
|
});
|
|
await ChatRepository.upsertReadIndicator({
|
|
userId: users.id(1),
|
|
roomId: room.id,
|
|
lastSeenMessageId: 3,
|
|
});
|
|
|
|
expect((await readIndicator(users.id(1), room.id)).lastSeenMessageId).toBe(
|
|
5,
|
|
);
|
|
|
|
await ChatRepository.upsertReadIndicator({
|
|
userId: users.id(1),
|
|
roomId: room.id,
|
|
lastSeenMessageId: 9,
|
|
});
|
|
|
|
expect((await readIndicator(users.id(1), room.id)).lastSeenMessageId).toBe(
|
|
9,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.updateRoomsInactive", () => {
|
|
test("marks the given rooms inactive and back active", async () => {
|
|
const room = await ChatRoomFactory.create();
|
|
const otherRoom = await ChatRoomFactory.create();
|
|
|
|
await ChatRepository.updateRoomsInactive([room.id], true);
|
|
|
|
expect((await roomById(room.id)).inactive).toBe(1);
|
|
expect((await roomById(otherRoom.id)).inactive).toBe(0);
|
|
|
|
await ChatRepository.updateRoomsInactive([room.id], false);
|
|
|
|
expect((await roomById(room.id)).inactive).toBe(0);
|
|
});
|
|
|
|
test("ignores null room ids", async () => {
|
|
await expect(
|
|
ChatRepository.updateRoomsInactive([null, null], true),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.closeExpiredRooms", () => {
|
|
test("closes only rooms expired before the cutoff", async () => {
|
|
const expiredRoom = await ChatRoomFactory.create({
|
|
expiresAt: subDays(new Date(), 60),
|
|
});
|
|
const openRoom = await ChatRoomFactory.create({
|
|
expiresAt: addDays(new Date(), 1),
|
|
});
|
|
|
|
const closedCount = await ChatRepository.closeExpiredRooms(
|
|
subDays(new Date(), 30),
|
|
);
|
|
|
|
expect(closedCount).toBe(1);
|
|
expect((await roomById(expiredRoom.id)).closedAt).not.toBeNull();
|
|
expect((await roomById(openRoom.id)).closedAt).toBeNull();
|
|
});
|
|
|
|
test("leaves already-closed rooms untouched", async () => {
|
|
await ChatRoomFactory.create({ expiresAt: subDays(new Date(), 60) });
|
|
await ChatRepository.closeExpiredRooms(subDays(new Date(), 30));
|
|
|
|
const closedAgainCount = await ChatRepository.closeExpiredRooms(
|
|
subDays(new Date(), 30),
|
|
);
|
|
|
|
expect(closedAgainCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("ChatRepository.deleteOrphanedRooms", () => {
|
|
// each type is checked against its own owner table alone, so every one needs covering
|
|
test.each([
|
|
"SQ_GROUP",
|
|
"SQ_MATCH",
|
|
"TOURNAMENT_MATCH",
|
|
"TOURNAMENT_TEAM",
|
|
"SCRIM",
|
|
] as const)("deletes an orphaned %s room", async (type) => {
|
|
const orphanedRoom = await ChatRoomFactory.create({ type });
|
|
|
|
const deletedCount = await ChatRepository.deleteOrphanedRooms();
|
|
|
|
expect(deletedCount).toBe(1);
|
|
await expect(roomById(orphanedRoom.id)).rejects.toThrow();
|
|
});
|
|
|
|
test("keeps a room its owner still points at", async () => {
|
|
const group = await SQGroupFactory.create({
|
|
memberUserIds: [users.id(1)],
|
|
});
|
|
const chatRoomId = await groupChatRoomId(group.id);
|
|
|
|
const deletedCount = await ChatRepository.deleteOrphanedRooms();
|
|
|
|
expect(deletedCount).toBe(0);
|
|
expect((await roomById(chatRoomId)).id).toBe(chatRoomId);
|
|
});
|
|
});
|
|
|
|
const groupChatRoomId = async (groupId: number) => {
|
|
const group = await db
|
|
.selectFrom("Group")
|
|
.select("Group.chatRoomId")
|
|
.where("Group.id", "=", groupId)
|
|
.executeTakeFirstOrThrow();
|
|
|
|
invariant(group.chatRoomId, "Group has no chat room");
|
|
|
|
return group.chatRoomId;
|
|
};
|
|
|
|
const readIndicator = (userId: number, roomId: number) =>
|
|
db
|
|
.selectFrom("ChatMessageReadIndicator")
|
|
.selectAll()
|
|
.where("userId", "=", userId)
|
|
.where("roomId", "=", roomId)
|
|
.executeTakeFirstOrThrow();
|
|
|
|
const roomById = (id: number) =>
|
|
db
|
|
.selectFrom("ChatRoom")
|
|
.selectAll()
|
|
.where("id", "=", id)
|
|
.executeTakeFirstOrThrow();
|