@openora/core 0.4.1-canary.71 → 0.4.1-canary.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/admin-console/__tests__/backoffice.service.int.test.js +30 -12
- package/dist/admin-console/__tests__/backoffice.service.int.test.js.map +1 -1
- package/dist/admin-console/contract/index.d.ts +4 -0
- package/dist/admin-console/contract/index.d.ts.map +1 -1
- package/dist/admin-console/contract/index.js +3 -2
- package/dist/admin-console/contract/index.js.map +1 -1
- package/dist/admin-console/plugin.d.ts +1 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/admin-console/router/index.d.ts +2 -0
- package/dist/admin-console/router/index.d.ts.map +1 -1
- package/dist/admin-console/service/backoffice.service.d.ts +2 -0
- package/dist/admin-console/service/backoffice.service.d.ts.map +1 -1
- package/dist/admin-console/service/backoffice.service.js +2 -0
- package/dist/admin-console/service/backoffice.service.js.map +1 -1
- package/dist/analytics/plugin.d.ts +1 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +1 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.js +6 -0
- package/dist/audit/plugin.js.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +1 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +1 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +1 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +1 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/adapters/admin-user-directory.d.ts +4 -2
- package/dist/contracts/adapters/admin-user-directory.d.ts.map +1 -1
- package/dist/contracts/adapters/admin-user-directory.js.map +1 -1
- package/dist/contracts/adapters/chat-moderation.d.ts +75 -0
- package/dist/contracts/adapters/chat-moderation.d.ts.map +1 -0
- package/dist/contracts/adapters/chat-moderation.js +3 -0
- package/dist/contracts/adapters/chat-moderation.js.map +1 -0
- package/dist/contracts/adapters/index.d.ts +2 -0
- package/dist/contracts/adapters/index.d.ts.map +1 -1
- package/dist/contracts/adapters/index.js +1 -0
- package/dist/contracts/adapters/index.js.map +1 -1
- package/dist/contracts/adapters/realtime.d.ts +9 -1
- package/dist/contracts/adapters/realtime.d.ts.map +1 -1
- package/dist/contracts/adapters/realtime.js.map +1 -1
- package/dist/contracts/schemas/events.d.ts +19 -4
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +12 -5
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +315 -19
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/contract/constants.d.ts +1 -1
- package/dist/engagement/chat/contract/constants.d.ts.map +1 -1
- package/dist/engagement/chat/contract/constants.js +1 -1
- package/dist/engagement/chat/contract/constants.js.map +1 -1
- package/dist/engagement/chat/contract/index.d.ts +764 -18
- package/dist/engagement/chat/contract/index.d.ts.map +1 -1
- package/dist/engagement/chat/contract/index.js +236 -5
- package/dist/engagement/chat/contract/index.js.map +1 -1
- package/dist/engagement/chat/drizzle/migrations/0005_tranquil_gwen_stacy.sql +100 -0
- package/dist/engagement/chat/drizzle/migrations/0006_tiny_ultron.sql +3 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0005_snapshot.json +1422 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0006_snapshot.json +1456 -0
- package/dist/engagement/chat/drizzle/migrations/meta/_journal.json +14 -0
- package/dist/engagement/chat/plugin.d.ts +2 -1
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/plugin.js +25 -13
- package/dist/engagement/chat/plugin.js.map +1 -1
- package/dist/engagement/chat/router/index.d.ts +518 -18
- package/dist/engagement/chat/router/index.d.ts.map +1 -1
- package/dist/engagement/chat/router/index.js +140 -20
- package/dist/engagement/chat/router/index.js.map +1 -1
- package/dist/engagement/chat/schema/index.d.ts +1017 -16
- package/dist/engagement/chat/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat/schema/index.js +110 -4
- package/dist/engagement/chat/schema/index.js.map +1 -1
- package/dist/engagement/chat/service/chat-ban.service.d.ts +26 -0
- package/dist/engagement/chat/service/chat-ban.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-ban.service.js +139 -0
- package/dist/engagement/chat/service/chat-ban.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.d.ts +14 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.js +48 -0
- package/dist/engagement/chat/service/chat-message-moderation.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-moderation.service.d.ts +30 -0
- package/dist/engagement/chat/service/chat-moderation.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-moderation.service.js +40 -0
- package/dist/engagement/chat/service/chat-moderation.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-mute.service.d.ts +26 -0
- package/dist/engagement/chat/service/chat-mute.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-mute.service.js +143 -0
- package/dist/engagement/chat/service/chat-mute.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-room-ban.service.d.ts +29 -0
- package/dist/engagement/chat/service/chat-room-ban.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-room-ban.service.js +105 -0
- package/dist/engagement/chat/service/chat-room-ban.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-room-membership.service.d.ts +72 -0
- package/dist/engagement/chat/service/chat-room-membership.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-room-membership.service.js +176 -0
- package/dist/engagement/chat/service/chat-room-membership.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-room-mute.service.d.ts +25 -0
- package/dist/engagement/chat/service/chat-room-mute.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-room-mute.service.js +89 -0
- package/dist/engagement/chat/service/chat-room-mute.service.js.map +1 -0
- package/dist/engagement/chat/service/chat.service.d.ts +348 -61
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +460 -194
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/chat/service/errors/chat-moderation.errors.d.ts +21 -0
- package/dist/engagement/chat/service/errors/chat-moderation.errors.d.ts.map +1 -0
- package/dist/engagement/chat/service/errors/chat-moderation.errors.js +31 -0
- package/dist/engagement/chat/service/errors/chat-moderation.errors.js.map +1 -0
- package/dist/engagement/chat-commands/contract/index.js +1 -1
- package/dist/engagement/chat-commands/contract/index.js.map +1 -1
- package/dist/engagement/chat-commands/drizzle/migrations/0001_sturdy_barracuda.sql +1 -0
- package/dist/engagement/chat-commands/drizzle/migrations/meta/0001_snapshot.json +83 -0
- package/dist/engagement/chat-commands/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/engagement/chat-commands/plugin.d.ts +1 -0
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/chat-commands/schema/index.d.ts +0 -199
- package/dist/engagement/chat-commands/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat-commands/schema/index.js +1 -19
- package/dist/engagement/chat-commands/schema/index.js.map +1 -1
- package/dist/engagement/chat-commands/service/__tests__/chat-commands.service.test.js +15 -1
- package/dist/engagement/chat-commands/service/__tests__/chat-commands.service.test.js.map +1 -1
- package/dist/engagement/chat-commands/service/chat-commands.service.d.ts +1 -0
- package/dist/engagement/chat-commands/service/chat-commands.service.d.ts.map +1 -1
- package/dist/engagement/chat-commands/service/chat-commands.service.js +38 -3
- package/dist/engagement/chat-commands/service/chat-commands.service.js.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts +1 -0
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/social/plugin.d.ts +1 -0
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/engagement/social-transfers/plugin.d.ts +1 -0
- package/dist/engagement/social-transfers/plugin.d.ts.map +1 -1
- package/dist/engagement/social-transfers/schema/index.d.ts +1 -2
- package/dist/engagement/social-transfers/schema/index.d.ts.map +1 -1
- package/dist/engagement/social-transfers/schema/index.js +1 -2
- package/dist/engagement/social-transfers/schema/index.js.map +1 -1
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.js +19 -0
- package/dist/engagement/social-transfers/service/__tests__/social-transfers.service.test.js.map +1 -1
- package/dist/iam/plugin.d.ts +1 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/iam/seed/data/default-admin-roles.d.ts.map +1 -1
- package/dist/iam/seed/data/default-admin-roles.js +3 -0
- package/dist/iam/seed/data/default-admin-roles.js.map +1 -1
- package/dist/pam/identity/__tests__/admin-user-directory.int.test.js +33 -3
- package/dist/pam/identity/__tests__/admin-user-directory.int.test.js.map +1 -1
- package/dist/pam/identity/admin-user-directory.d.ts +12 -0
- package/dist/pam/identity/admin-user-directory.d.ts.map +1 -1
- package/dist/pam/identity/admin-user-directory.js +25 -25
- package/dist/pam/identity/admin-user-directory.js.map +1 -1
- package/dist/pam/identity/plugin.d.ts +1 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/__tests__/player.service.int.test.js +4 -0
- package/dist/pam/player-management/__tests__/player.service.int.test.js.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +1 -0
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +1 -0
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/plugin.d.ts +1 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/tag/plugin.d.ts +1 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/auth/permissions.d.ts +12 -12
- package/dist/server/auth/permissions.js +2 -2
- package/dist/server/auth/permissions.js.map +1 -1
- package/dist/server/kernel/__tests__/realtime-transport.test.js +7 -0
- package/dist/server/kernel/__tests__/realtime-transport.test.js.map +1 -1
- package/dist/server/kernel/orpc-error-map.d.ts.map +1 -1
- package/dist/server/kernel/orpc-error-map.js +4 -1
- package/dist/server/kernel/orpc-error-map.js.map +1 -1
- package/dist/server/kernel/realtime-transport.d.ts +3 -1
- package/dist/server/kernel/realtime-transport.d.ts.map +1 -1
- package/dist/server/kernel/realtime-transport.js +22 -4
- package/dist/server/kernel/realtime-transport.js.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +1 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.js +2 -1
- package/dist/server/runtime/core-token-catalog.js.map +1 -1
- package/dist/testing/realtime-transport-conformance.d.ts.map +1 -1
- package/dist/testing/realtime-transport-conformance.js +4 -0
- package/dist/testing/realtime-transport-conformance.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.int.test.js +19 -2
- package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
- package/dist/wallet/contract/index.d.ts +2 -0
- package/dist/wallet/contract/index.d.ts.map +1 -1
- package/dist/wallet/contract/index.js +1 -0
- package/dist/wallet/contract/index.js.map +1 -1
- package/dist/wallet/plugin.d.ts +1 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/dist/wallet/router/index.d.ts +1 -0
- package/dist/wallet/router/index.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.d.ts +1 -0
- package/dist/wallet/service/wallet.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.js +1 -0
- package/dist/wallet/service/wallet.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { eq, sql } from 'drizzle-orm';
|
|
3
|
+
import { and, eq, isNull, sql } from 'drizzle-orm';
|
|
4
4
|
import { createTestDb, InProcessRealtimeTransport } from '@openora/core/testing';
|
|
5
5
|
import { user } from '@openora/core/pam/schema/identity';
|
|
6
6
|
import { migrate as migrateIdentity } from '@openora/core/pam/migrate/identity';
|
|
@@ -18,16 +18,42 @@ import { migrate } from '../migrate.js';
|
|
|
18
18
|
// NO_CLIENT_META,
|
|
19
19
|
// } from '../../../testing/mock.js';
|
|
20
20
|
import { chatChannel } from '@openora/core/contracts';
|
|
21
|
-
import { ChatService,
|
|
22
|
-
import {
|
|
21
|
+
import { ChatService, ChatMessageBlockedError, ChatSelfBlockError, ChatSelfIgnoreError, ChatRoomSlugConflictError, ChatRoomJoinCodeNotFoundError, ChatRoomBannedError, ChatRoomLimitReachedError, ChatRoomLastModeratorError, ChatRoomNotMemberError, ChatRoomNotModeratorError, ChatRoomSelfModerationError, ChatRoomOwnershipError, } from '../service/chat.service.js';
|
|
22
|
+
import { ChatModerationService, ChatRoomNotFoundError, ChatMessageNotFoundError, ChatPlayerMutedError, ChatPlayerBannedError, ChatAdminPrivateRoomModerationError, } from '../service/chat-moderation.service.js';
|
|
23
|
+
import { chatMessage, chatRoom, chatRoomMember, chatRoomBan, chatRoomRule, chatRoomConfiguration, chatRoomMute, chatRoomRemove, chatUserBlock, chatUserIgnore, chatMute, chatPlatformBan, } from '../schema/index.js';
|
|
24
|
+
import { ChatRoomMembershipService } from '../service/chat-room-membership.service.js';
|
|
25
|
+
import { ChatRoomBanService } from '../service/chat-room-ban.service.js';
|
|
26
|
+
import { ChatRoomMuteService } from '../service/chat-room-mute.service.js';
|
|
23
27
|
let db;
|
|
24
28
|
function makeService(directory = mock({ lookupPlayers: async () => [] })) {
|
|
25
29
|
const transport = new InProcessRealtimeTransport();
|
|
26
30
|
const events = makeEventBus();
|
|
31
|
+
const audit = mock({
|
|
32
|
+
record: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
recordInTransaction: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
});
|
|
35
|
+
const moderation = new ChatModerationService(db.drizzle, transport, audit);
|
|
36
|
+
const identityReader = makeIdentityReader();
|
|
37
|
+
const chatService = new ChatService(db.drizzle, events, transport, directory, audit, moderation, identityReader);
|
|
38
|
+
const membership = new ChatRoomMembershipService(db.drizzle, events, audit, transport, identityReader);
|
|
39
|
+
const roomBan = new ChatRoomBanService(db.drizzle, events, audit, transport, identityReader);
|
|
40
|
+
const roomMute = new ChatRoomMuteService(db.drizzle, audit);
|
|
27
41
|
return {
|
|
28
|
-
|
|
42
|
+
moderation,
|
|
43
|
+
svc: Object.assign(chatService, {
|
|
44
|
+
joinRoom: membership.joinRoom.bind(membership),
|
|
45
|
+
joinPublicRoom: membership.joinPublicRoom.bind(membership),
|
|
46
|
+
adminJoinRoom: membership.adminJoinRoom.bind(membership),
|
|
47
|
+
leaveRoom: membership.leaveRoom.bind(membership),
|
|
48
|
+
removeMember: membership.removeMember.bind(membership),
|
|
49
|
+
banMember: roomBan.banMember.bind(roomBan),
|
|
50
|
+
unbanMember: roomBan.unbanMember.bind(roomBan),
|
|
51
|
+
muteRoomMember: roomMute.muteRoomMember.bind(roomMute),
|
|
52
|
+
unmuteRoomMember: roomMute.unmuteRoomMember.bind(roomMute),
|
|
53
|
+
}),
|
|
29
54
|
events,
|
|
30
55
|
transport,
|
|
56
|
+
audit,
|
|
31
57
|
};
|
|
32
58
|
}
|
|
33
59
|
async function seedUser(name = 'Player') {
|
|
@@ -78,7 +104,7 @@ afterAll(async () => {
|
|
|
78
104
|
await db.drop();
|
|
79
105
|
});
|
|
80
106
|
beforeEach(async () => {
|
|
81
|
-
await db.drizzle.db.execute(sql `TRUNCATE ${chatMessage}, ${chatRoomMember}, ${chatRoomBan}, ${chatUserBlock}, ${chatUserIgnore}, ${chatRoom}, ${user} RESTART IDENTITY CASCADE`);
|
|
107
|
+
await db.drizzle.db.execute(sql `TRUNCATE ${chatMessage}, ${chatRoomRule}, ${chatRoomConfiguration}, ${chatRoomMember}, ${chatRoomBan}, ${chatRoomMute}, ${chatRoomRemove}, ${chatMute}, ${chatPlatformBan}, ${chatUserBlock}, ${chatUserIgnore}, ${chatRoom}, ${user} RESTART IDENTITY CASCADE`);
|
|
82
108
|
});
|
|
83
109
|
describe('Chat contract surface', () => {
|
|
84
110
|
it('exposes the complete persisted category set', () => {
|
|
@@ -314,24 +340,37 @@ describe('ChatService.sendRoomMessage (real PG)', () => {
|
|
|
314
340
|
});
|
|
315
341
|
});
|
|
316
342
|
describe('ChatService.deleteMessage (real PG)', () => {
|
|
317
|
-
it('soft-deletes
|
|
343
|
+
it('soft-deletes a message for a room owner and publishes a tombstone', async () => {
|
|
318
344
|
const { svc } = makeService();
|
|
345
|
+
const ownerId = randomUUID();
|
|
346
|
+
const globalRoom = await seedRoom({ slug: '__global', isPublic: true });
|
|
347
|
+
await db.drizzle.db
|
|
348
|
+
.insert(chatRoomMember)
|
|
349
|
+
.values({ roomId: globalRoom.id, userId: ownerId, role: 'owner' });
|
|
319
350
|
const message = await seedMessage();
|
|
320
|
-
await expect(svc.deleteMessage(message.id,
|
|
351
|
+
await expect(svc.deleteMessage(message.id, ownerId)).resolves.toEqual({ success: true });
|
|
321
352
|
const [stored] = await db.drizzle.db
|
|
322
353
|
.select()
|
|
323
354
|
.from(chatMessage)
|
|
324
355
|
.where(eq(chatMessage.id, message.id));
|
|
325
356
|
expect(stored?.isDeleted).toBe(true);
|
|
357
|
+
expect(stored?.deletedAt).toBeInstanceOf(Date);
|
|
326
358
|
});
|
|
327
359
|
it('throws ChatMessageNotFoundError for an unknown id', async () => {
|
|
328
360
|
const { svc } = makeService();
|
|
329
361
|
await expect(svc.deleteMessage(randomUUID(), randomUUID())).rejects.toBeInstanceOf(ChatMessageNotFoundError);
|
|
330
362
|
});
|
|
331
|
-
it('refuses
|
|
363
|
+
it('refuses deletion by a regular room member', async () => {
|
|
332
364
|
const { svc } = makeService();
|
|
365
|
+
const ownerId = randomUUID();
|
|
366
|
+
const memberId = randomUUID();
|
|
367
|
+
const globalRoom = await seedRoom({ slug: '__global', isPublic: true });
|
|
368
|
+
await db.drizzle.db.insert(chatRoomMember).values([
|
|
369
|
+
{ roomId: globalRoom.id, userId: ownerId, role: 'owner' },
|
|
370
|
+
{ roomId: globalRoom.id, userId: memberId, role: 'member' },
|
|
371
|
+
]);
|
|
333
372
|
const message = await seedMessage();
|
|
334
|
-
await expect(svc.deleteMessage(message.id,
|
|
373
|
+
await expect(svc.deleteMessage(message.id, memberId)).rejects.toBeInstanceOf(ChatRoomNotModeratorError);
|
|
335
374
|
});
|
|
336
375
|
});
|
|
337
376
|
describe('ChatService block list (real PG)', () => {
|
|
@@ -634,6 +673,11 @@ describe('ChatService admin rooms (real PG)', () => {
|
|
|
634
673
|
});
|
|
635
674
|
expect(room).toMatchObject({ slug: 'jackpot-wheel', isPublic: true });
|
|
636
675
|
expect(typeof room.createdAt).toBe('string');
|
|
676
|
+
const [configuration] = await db.drizzle.db
|
|
677
|
+
.select()
|
|
678
|
+
.from(chatRoomConfiguration)
|
|
679
|
+
.where(eq(chatRoomConfiguration.roomId, room.id));
|
|
680
|
+
expect(configuration).toMatchObject({ roomId: room.id });
|
|
637
681
|
expect(events.emit).toHaveBeenCalledWith('chat.room.created', expect.objectContaining({ roomId: room.id, slug: 'jackpot-wheel' }));
|
|
638
682
|
});
|
|
639
683
|
it('throws ChatRoomSlugConflictError when the slug is taken', async () => {
|
|
@@ -679,6 +723,18 @@ describe('ChatService admin rooms (real PG)', () => {
|
|
|
679
723
|
after: expect.objectContaining({ name: 'New', category: 'regions' }),
|
|
680
724
|
}));
|
|
681
725
|
});
|
|
726
|
+
it('allows only the private-room creator to update its name', async () => {
|
|
727
|
+
const { svc } = makeService();
|
|
728
|
+
const ownerId = randomUUID();
|
|
729
|
+
const room = await svc.createPrivateRoom({ userId: ownerId, name: 'Old', ...NO_CLIENT_META });
|
|
730
|
+
await expect(svc.updatePrivateRoom({ id: room.id, name: 'New', actorId: ownerId, ...NO_CLIENT_META })).resolves.toMatchObject({ id: room.id, name: 'New' });
|
|
731
|
+
await expect(svc.updatePrivateRoom({
|
|
732
|
+
id: room.id,
|
|
733
|
+
name: 'Hijacked',
|
|
734
|
+
actorId: randomUUID(),
|
|
735
|
+
...NO_CLIENT_META,
|
|
736
|
+
})).rejects.toBeInstanceOf(ChatRoomOwnershipError);
|
|
737
|
+
});
|
|
682
738
|
it('rejects an update that collides with another rooms slug', async () => {
|
|
683
739
|
const { svc } = makeService();
|
|
684
740
|
await seedRoom({ slug: 'taken' });
|
|
@@ -702,6 +758,26 @@ describe('ChatService admin rooms (real PG)', () => {
|
|
|
702
758
|
await svc.deleteRoom(room.id);
|
|
703
759
|
await expect(svc.deleteRoom(room.id)).rejects.toBeInstanceOf(ChatRoomNotFoundError);
|
|
704
760
|
});
|
|
761
|
+
it('ends a private room only for its owner and emits auditable before/after state', async () => {
|
|
762
|
+
const { svc, events } = makeService();
|
|
763
|
+
const ownerId = randomUUID();
|
|
764
|
+
const otherUserId = randomUUID();
|
|
765
|
+
const room = await svc.createPrivateRoom({
|
|
766
|
+
userId: ownerId,
|
|
767
|
+
name: 'End me',
|
|
768
|
+
...NO_CLIENT_META,
|
|
769
|
+
});
|
|
770
|
+
await expect(svc.deletePrivateRoom({ roomId: room.id, userId: otherUserId, ...NO_CLIENT_META })).rejects.toBeInstanceOf(ChatRoomOwnershipError);
|
|
771
|
+
await expect(svc.deletePrivateRoom({ roomId: room.id, userId: ownerId, ...NO_CLIENT_META })).resolves.toEqual({ success: true });
|
|
772
|
+
const [stored] = await db.drizzle.db.select().from(chatRoom).where(eq(chatRoom.id, room.id));
|
|
773
|
+
expect(stored?.deletedAt).toBeInstanceOf(Date);
|
|
774
|
+
expect(events.emit).toHaveBeenCalledWith('chat.private_room.deleted', expect.objectContaining({
|
|
775
|
+
roomId: room.id,
|
|
776
|
+
creatorId: ownerId,
|
|
777
|
+
before: { name: 'End me', slug: room.slug, category: 'private-channels' },
|
|
778
|
+
after: { deletedAt: expect.any(String) },
|
|
779
|
+
}));
|
|
780
|
+
});
|
|
705
781
|
});
|
|
706
782
|
describe('ChatService.createPrivateRoom (real PG)', () => {
|
|
707
783
|
it('creates the room, joins the creator as moderator, and issues a join code', async () => {
|
|
@@ -716,7 +792,7 @@ describe('ChatService.createPrivateRoom (real PG)', () => {
|
|
|
716
792
|
.select()
|
|
717
793
|
.from(chatRoomMember)
|
|
718
794
|
.where(eq(chatRoomMember.roomId, room.id));
|
|
719
|
-
expect(member).toMatchObject({ userId, role: '
|
|
795
|
+
expect(member).toMatchObject({ userId, role: 'owner' });
|
|
720
796
|
expect(events.emit).toHaveBeenCalledWith('chat.private_room.created', expect.objectContaining({ roomId: room.id, creatorId: userId }));
|
|
721
797
|
});
|
|
722
798
|
it('rejects one room past the per-player cap', async () => {
|
|
@@ -751,6 +827,37 @@ describe('ChatService.createPrivateRoom (real PG)', () => {
|
|
|
751
827
|
});
|
|
752
828
|
});
|
|
753
829
|
describe('ChatService.joinRoom (real PG)', () => {
|
|
830
|
+
it('joins a public room by its room id', async () => {
|
|
831
|
+
const { svc } = makeService();
|
|
832
|
+
const room = await seedRoom({ isPublic: true, joinCode: 'PUB123' });
|
|
833
|
+
const userId = randomUUID();
|
|
834
|
+
await expect(svc.joinPublicRoom({ roomId: room.id, userId, ...NO_CLIENT_META })).resolves.toMatchObject({ id: room.id });
|
|
835
|
+
const members = await db.drizzle.db
|
|
836
|
+
.select()
|
|
837
|
+
.from(chatRoomMember)
|
|
838
|
+
.where(eq(chatRoomMember.roomId, room.id));
|
|
839
|
+
expect(members.map((member) => member.userId)).toContain(userId);
|
|
840
|
+
});
|
|
841
|
+
it('creates a default-disabled configuration for new private rooms', async () => {
|
|
842
|
+
const { svc } = makeService();
|
|
843
|
+
const room = await svc.createPrivateRoom({
|
|
844
|
+
userId: randomUUID(),
|
|
845
|
+
name: 'Configured room',
|
|
846
|
+
...NO_CLIENT_META,
|
|
847
|
+
});
|
|
848
|
+
const [configuration] = await db.drizzle.db
|
|
849
|
+
.select()
|
|
850
|
+
.from(chatRoomConfiguration)
|
|
851
|
+
.where(eq(chatRoomConfiguration.roomId, room.id));
|
|
852
|
+
expect(configuration).toMatchObject({
|
|
853
|
+
slowMode: false,
|
|
854
|
+
slowModeSeconds: 0,
|
|
855
|
+
readOnlyMode: false,
|
|
856
|
+
onlyInvitedCanJoin: false,
|
|
857
|
+
lockRoom: false,
|
|
858
|
+
moderatorInvite: false,
|
|
859
|
+
});
|
|
860
|
+
});
|
|
754
861
|
it('joins by code and records the membership once', async () => {
|
|
755
862
|
const { svc, events } = makeService();
|
|
756
863
|
const creatorId = randomUUID();
|
|
@@ -819,7 +926,7 @@ describe('ChatService.leaveRoom (real PG)', () => {
|
|
|
819
926
|
});
|
|
820
927
|
describe('ChatService moderation (real PG)', () => {
|
|
821
928
|
async function roomWithMember() {
|
|
822
|
-
const { svc, events } = makeService();
|
|
929
|
+
const { svc, events, audit, moderation, transport } = makeService();
|
|
823
930
|
const moderatorId = randomUUID();
|
|
824
931
|
const room = await svc.createPrivateRoom({
|
|
825
932
|
userId: moderatorId,
|
|
@@ -828,16 +935,20 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
828
935
|
});
|
|
829
936
|
const memberId = randomUUID();
|
|
830
937
|
await svc.joinRoom({ userId: memberId, joinCode: room.joinCode, ...NO_CLIENT_META });
|
|
831
|
-
return { svc, events, room, moderatorId, memberId };
|
|
938
|
+
return { svc, events, room, moderatorId, memberId, audit, moderation, transport };
|
|
832
939
|
}
|
|
833
940
|
it('kicks a member, who can then rejoin with the code', async () => {
|
|
834
|
-
const { svc, room, moderatorId, memberId } = await roomWithMember();
|
|
835
|
-
|
|
941
|
+
const { svc, room, moderatorId, memberId, transport } = await roomWithMember();
|
|
942
|
+
const received = [];
|
|
943
|
+
transport.subscribe(chatChannel(room.id), () => received.push(true), memberId);
|
|
944
|
+
await svc.removeMember({ moderatorId, roomId: room.id, userId: memberId, ...NO_CLIENT_META });
|
|
836
945
|
const afterKick = await db.drizzle.db
|
|
837
946
|
.select()
|
|
838
947
|
.from(chatRoomMember)
|
|
839
948
|
.where(eq(chatRoomMember.roomId, room.id));
|
|
840
949
|
expect(afterKick.map((m) => m.userId)).not.toContain(memberId);
|
|
950
|
+
transport.publish(chatChannel(room.id), { type: 'chat.message.sent' });
|
|
951
|
+
expect(received).toEqual([]);
|
|
841
952
|
await expect(svc.joinRoom({ userId: memberId, joinCode: room.joinCode, ...NO_CLIENT_META })).resolves.toMatchObject({ id: room.id });
|
|
842
953
|
});
|
|
843
954
|
it('bans a member, removing membership and blocking a rejoin', async () => {
|
|
@@ -856,16 +967,50 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
856
967
|
const { svc, room, memberId } = await roomWithMember();
|
|
857
968
|
const otherId = randomUUID();
|
|
858
969
|
await svc.joinRoom({ userId: otherId, joinCode: room.joinCode, ...NO_CLIENT_META });
|
|
859
|
-
await expect(svc.
|
|
970
|
+
await expect(svc.removeMember({
|
|
860
971
|
moderatorId: memberId,
|
|
861
972
|
roomId: room.id,
|
|
862
973
|
userId: otherId,
|
|
863
974
|
...NO_CLIENT_META,
|
|
864
975
|
})).rejects.toBeInstanceOf(ChatRoomNotModeratorError);
|
|
865
976
|
});
|
|
977
|
+
it('allows an owner to moderate like a moderator', async () => {
|
|
978
|
+
const { svc, room, memberId } = await roomWithMember();
|
|
979
|
+
const ownerId = randomUUID();
|
|
980
|
+
await db.drizzle.db
|
|
981
|
+
.insert(chatRoomMember)
|
|
982
|
+
.values({ roomId: room.id, userId: ownerId, role: 'owner' });
|
|
983
|
+
await expect(svc.removeMember({
|
|
984
|
+
moderatorId: ownerId,
|
|
985
|
+
roomId: room.id,
|
|
986
|
+
userId: memberId,
|
|
987
|
+
...NO_CLIENT_META,
|
|
988
|
+
})).resolves.toEqual({ success: true });
|
|
989
|
+
});
|
|
990
|
+
it('prevents a moderator from removing another moderator or the owner', async () => {
|
|
991
|
+
const { svc, room, moderatorId, memberId } = await roomWithMember();
|
|
992
|
+
const otherModeratorId = randomUUID();
|
|
993
|
+
await db.drizzle.db.insert(chatRoomMember).values({
|
|
994
|
+
roomId: room.id,
|
|
995
|
+
userId: otherModeratorId,
|
|
996
|
+
role: 'moderator',
|
|
997
|
+
});
|
|
998
|
+
await expect(svc.removeMember({
|
|
999
|
+
moderatorId: otherModeratorId,
|
|
1000
|
+
roomId: room.id,
|
|
1001
|
+
userId: moderatorId,
|
|
1002
|
+
...NO_CLIENT_META,
|
|
1003
|
+
})).rejects.toBeInstanceOf(ChatRoomNotModeratorError);
|
|
1004
|
+
await expect(svc.removeMember({
|
|
1005
|
+
moderatorId: otherModeratorId,
|
|
1006
|
+
roomId: room.id,
|
|
1007
|
+
userId: memberId,
|
|
1008
|
+
...NO_CLIENT_META,
|
|
1009
|
+
})).resolves.toEqual({ success: true });
|
|
1010
|
+
});
|
|
866
1011
|
it('refuses moderation by a non-member', async () => {
|
|
867
1012
|
const { svc, room, memberId } = await roomWithMember();
|
|
868
|
-
await expect(svc.
|
|
1013
|
+
await expect(svc.removeMember({
|
|
869
1014
|
moderatorId: randomUUID(),
|
|
870
1015
|
roomId: room.id,
|
|
871
1016
|
userId: memberId,
|
|
@@ -874,7 +1019,7 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
874
1019
|
});
|
|
875
1020
|
it('refuses self-moderation', async () => {
|
|
876
1021
|
const { svc, room, moderatorId } = await roomWithMember();
|
|
877
|
-
await expect(svc.
|
|
1022
|
+
await expect(svc.removeMember({
|
|
878
1023
|
moderatorId,
|
|
879
1024
|
roomId: room.id,
|
|
880
1025
|
userId: moderatorId,
|
|
@@ -887,6 +1032,156 @@ describe('ChatService moderation (real PG)', () => {
|
|
|
887
1032
|
...NO_CLIENT_META,
|
|
888
1033
|
})).rejects.toBeInstanceOf(ChatRoomSelfModerationError);
|
|
889
1034
|
});
|
|
1035
|
+
it('allows muted users to read but rejects sends for the configured channel', async () => {
|
|
1036
|
+
const { svc, audit, moderation } = makeService();
|
|
1037
|
+
const room = await seedRoom();
|
|
1038
|
+
const userId = randomUUID();
|
|
1039
|
+
await moderation.mute({
|
|
1040
|
+
userId,
|
|
1041
|
+
roomId: room.id,
|
|
1042
|
+
durationSeconds: 60,
|
|
1043
|
+
reason: 'spam',
|
|
1044
|
+
actorId: randomUUID(),
|
|
1045
|
+
...NO_CLIENT_META,
|
|
1046
|
+
});
|
|
1047
|
+
await expect(svc.getRoomMessages({ roomId: room.id, viewerId: userId })).resolves.toEqual([]);
|
|
1048
|
+
await expect(svc.sendRoomMessage({ userId, username: 'Muted', roomId: room.id, content: 'hello' })).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1049
|
+
expect(audit.record).toHaveBeenCalledWith(expect.objectContaining({ action: 'chat.mute.created', resourceType: 'chat_mute' }));
|
|
1050
|
+
});
|
|
1051
|
+
it('enforces global read-only mode on global sends', async () => {
|
|
1052
|
+
const { svc } = makeService();
|
|
1053
|
+
const globalRoom = await seedRoom({ slug: '__global' });
|
|
1054
|
+
await db.drizzle.db.insert(chatRoomConfiguration).values({
|
|
1055
|
+
roomId: globalRoom.id,
|
|
1056
|
+
readOnlyMode: true,
|
|
1057
|
+
});
|
|
1058
|
+
await expect(svc.sendGlobalMessage(randomUUID(), 'ReadOnly', 'hello')).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1059
|
+
});
|
|
1060
|
+
it.each(['__all_public', '__all'])('enforces %s mutes on global sends', async (scope) => {
|
|
1061
|
+
const { svc, moderation } = makeService();
|
|
1062
|
+
await moderation.mute({
|
|
1063
|
+
userId: randomUUID(),
|
|
1064
|
+
roomId: scope,
|
|
1065
|
+
durationSeconds: 60,
|
|
1066
|
+
reason: 'spam',
|
|
1067
|
+
actorId: randomUUID(),
|
|
1068
|
+
...NO_CLIENT_META,
|
|
1069
|
+
});
|
|
1070
|
+
const [mute] = await db.drizzle.db.select().from(chatMute).limit(1);
|
|
1071
|
+
await expect(svc.sendGlobalMessage(mute.userId, 'Muted', 'hello')).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1072
|
+
});
|
|
1073
|
+
it('enforces an all-chat mute in private rooms', async () => {
|
|
1074
|
+
const { svc, moderation } = makeService();
|
|
1075
|
+
const { room } = await roomWithMember();
|
|
1076
|
+
const userId = randomUUID();
|
|
1077
|
+
await svc.joinRoom({ userId, joinCode: room.joinCode, ...NO_CLIENT_META });
|
|
1078
|
+
await moderation.mute({
|
|
1079
|
+
userId,
|
|
1080
|
+
roomId: '__all',
|
|
1081
|
+
durationSeconds: 60,
|
|
1082
|
+
reason: 'spam',
|
|
1083
|
+
actorId: randomUUID(),
|
|
1084
|
+
...NO_CLIENT_META,
|
|
1085
|
+
});
|
|
1086
|
+
await expect(svc.sendRoomMessage({ userId, username: 'Muted', roomId: room.id, content: 'hello' })).rejects.toBeInstanceOf(ChatPlayerMutedError);
|
|
1087
|
+
});
|
|
1088
|
+
it('replaces an expired private-room mute instead of reusing it', async () => {
|
|
1089
|
+
const { svc, room, moderatorId, memberId } = await roomWithMember();
|
|
1090
|
+
await db.drizzle.db.insert(chatRoomMute).values({
|
|
1091
|
+
roomId: room.id,
|
|
1092
|
+
userId: memberId,
|
|
1093
|
+
mutedBy: moderatorId,
|
|
1094
|
+
reason: 'old reason',
|
|
1095
|
+
expiresAt: new Date(Date.now() - 1000),
|
|
1096
|
+
});
|
|
1097
|
+
await svc.muteRoomMember({
|
|
1098
|
+
roomId: room.id,
|
|
1099
|
+
userId: memberId,
|
|
1100
|
+
moderatorId,
|
|
1101
|
+
durationSeconds: 60,
|
|
1102
|
+
reason: 'new reason',
|
|
1103
|
+
});
|
|
1104
|
+
const rows = await db.drizzle.db
|
|
1105
|
+
.select()
|
|
1106
|
+
.from(chatRoomMute)
|
|
1107
|
+
.where(eq(chatRoomMute.roomId, room.id));
|
|
1108
|
+
expect(rows).toHaveLength(2);
|
|
1109
|
+
expect(rows.filter((row) => row.liftedAt)).toHaveLength(1);
|
|
1110
|
+
expect(rows.filter((row) => !row.liftedAt)).toHaveLength(1);
|
|
1111
|
+
});
|
|
1112
|
+
it('enforces a reversible platform ban only on public chat', async () => {
|
|
1113
|
+
const { svc, room, memberId, moderation } = await roomWithMember();
|
|
1114
|
+
const userId = memberId;
|
|
1115
|
+
await moderation.ban({
|
|
1116
|
+
userId,
|
|
1117
|
+
roomId: '__all_public',
|
|
1118
|
+
durationSeconds: null,
|
|
1119
|
+
reason: 'abuse',
|
|
1120
|
+
actorId: randomUUID(),
|
|
1121
|
+
...NO_CLIENT_META,
|
|
1122
|
+
});
|
|
1123
|
+
await expect(svc.sendGlobalMessage(userId, 'Banned', 'hello')).rejects.toBeInstanceOf(ChatPlayerBannedError);
|
|
1124
|
+
await expect(svc.sendRoomMessage({ userId, username: 'Banned', roomId: room.id, content: 'hello' })).resolves.toMatchObject({ userId });
|
|
1125
|
+
await moderation.unban({
|
|
1126
|
+
userId,
|
|
1127
|
+
roomId: '__all_public',
|
|
1128
|
+
actorId: randomUUID(),
|
|
1129
|
+
...NO_CLIENT_META,
|
|
1130
|
+
});
|
|
1131
|
+
await expect(svc.sendGlobalMessage(userId, 'Banned', 'hello')).resolves.toMatchObject({
|
|
1132
|
+
userId,
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
it('replaces expired admin bans and rejects private-room targets', async () => {
|
|
1136
|
+
const { svc, moderation } = makeService();
|
|
1137
|
+
const userId = randomUUID();
|
|
1138
|
+
const actorId = randomUUID();
|
|
1139
|
+
const room = await seedRoom();
|
|
1140
|
+
await db.drizzle.db.insert(chatPlatformBan).values({
|
|
1141
|
+
userId,
|
|
1142
|
+
bannedBy: actorId,
|
|
1143
|
+
scope: 'room',
|
|
1144
|
+
roomId: room.id,
|
|
1145
|
+
reason: 'old reason',
|
|
1146
|
+
expiresAt: new Date(Date.now() - 1000),
|
|
1147
|
+
});
|
|
1148
|
+
await moderation.ban({
|
|
1149
|
+
userId,
|
|
1150
|
+
roomId: room.id,
|
|
1151
|
+
durationSeconds: 60,
|
|
1152
|
+
reason: 'new reason',
|
|
1153
|
+
actorId,
|
|
1154
|
+
...NO_CLIENT_META,
|
|
1155
|
+
});
|
|
1156
|
+
const active = await db.drizzle.db
|
|
1157
|
+
.select()
|
|
1158
|
+
.from(chatPlatformBan)
|
|
1159
|
+
.where(and(eq(chatPlatformBan.userId, userId), isNull(chatPlatformBan.liftedAt)));
|
|
1160
|
+
expect(active).toHaveLength(1);
|
|
1161
|
+
const privateRoom = await svc.createPrivateRoom({
|
|
1162
|
+
userId: actorId,
|
|
1163
|
+
name: 'Private',
|
|
1164
|
+
...NO_CLIENT_META,
|
|
1165
|
+
});
|
|
1166
|
+
await expect(moderation.ban({
|
|
1167
|
+
userId,
|
|
1168
|
+
roomId: privateRoom.id,
|
|
1169
|
+
durationSeconds: null,
|
|
1170
|
+
reason: 'private target',
|
|
1171
|
+
actorId,
|
|
1172
|
+
...NO_CLIENT_META,
|
|
1173
|
+
})).rejects.toBeInstanceOf(ChatAdminPrivateRoomModerationError);
|
|
1174
|
+
});
|
|
1175
|
+
it('admin deletion publishes a tombstone and records audit data', async () => {
|
|
1176
|
+
const { svc, audit, moderation } = makeService();
|
|
1177
|
+
const message = await seedMessage({ content: 'bad content' });
|
|
1178
|
+
const received = [];
|
|
1179
|
+
svc.subscribeMessages(null, (event) => received.push(event));
|
|
1180
|
+
await moderation.deleteMessage(message.id, randomUUID(), NO_CLIENT_META);
|
|
1181
|
+
expect(received[0]).toMatchObject({ id: message.id, isDeleted: true });
|
|
1182
|
+
expect(audit.record).toHaveBeenCalledWith(expect.objectContaining({ action: 'chat.message.deleted', resourceId: message.id }));
|
|
1183
|
+
await expect(svc.getGlobalMessages()).resolves.toEqual([]);
|
|
1184
|
+
});
|
|
890
1185
|
});
|
|
891
1186
|
describe('ChatService.verifyRoomAccess and listings (real PG)', () => {
|
|
892
1187
|
it('returns a public room without checking membership', async () => {
|
|
@@ -930,6 +1225,7 @@ describe('ChatService.verifyRoomAccess and listings (real PG)', () => {
|
|
|
930
1225
|
const now = new Date();
|
|
931
1226
|
function summary(userId, username) {
|
|
932
1227
|
return {
|
|
1228
|
+
playerId: randomUUID(),
|
|
933
1229
|
userId,
|
|
934
1230
|
username,
|
|
935
1231
|
email: `${username}@example.test`,
|
|
@@ -958,7 +1254,7 @@ describe('ChatService.verifyRoomAccess and listings (real PG)', () => {
|
|
|
958
1254
|
{ userId: moderatorId, username: 'Moderator' },
|
|
959
1255
|
{ userId: memberId, username: 'SilentMember' },
|
|
960
1256
|
]);
|
|
961
|
-
expect(members[0]).toMatchObject({ role: '
|
|
1257
|
+
expect(members[0]).toMatchObject({ role: 'owner' });
|
|
962
1258
|
});
|
|
963
1259
|
it('returns username: null when the directory does not resolve a member', async () => {
|
|
964
1260
|
const directory = mock({ lookupPlayers: async () => [] });
|