@openora/core 0.4.1-canary.126 → 0.4.1-canary.128
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/plugin.d.ts +3 -0
- package/dist/admin-console/plugin.d.ts.map +1 -1
- package/dist/analytics/plugin.d.ts +3 -0
- package/dist/analytics/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.d.ts +3 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +3 -0
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/lobby/plugin.d.ts +3 -0
- package/dist/casino/lobby/plugin.d.ts.map +1 -1
- package/dist/cms/plugin.d.ts +3 -0
- package/dist/cms/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.d.ts +3 -0
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/contracts/adapters/audit.d.ts +1 -1
- package/dist/contracts/adapters/audit.d.ts.map +1 -1
- package/dist/contracts/adapters/audit.js.map +1 -1
- package/dist/contracts/adapters/player-provisioning.d.ts +8 -0
- package/dist/contracts/adapters/player-provisioning.d.ts.map +1 -1
- package/dist/contracts/adapters/player-provisioning.js.map +1 -1
- package/dist/contracts/schemas/__tests__/common-schemas.test.js +26 -1
- package/dist/contracts/schemas/__tests__/common-schemas.test.js.map +1 -1
- package/dist/contracts/schemas/common.d.ts +14 -0
- package/dist/contracts/schemas/common.d.ts.map +1 -1
- package/dist/contracts/schemas/common.js +22 -0
- package/dist/contracts/schemas/common.js.map +1 -1
- package/dist/contracts/schemas/identity.d.ts +5 -0
- package/dist/contracts/schemas/identity.d.ts.map +1 -1
- package/dist/contracts/schemas/identity.js +8 -1
- package/dist/contracts/schemas/identity.js.map +1 -1
- package/dist/contracts/schemas/platform-config.d.ts +24 -0
- package/dist/contracts/schemas/platform-config.d.ts.map +1 -1
- package/dist/contracts/schemas/platform-config.js +29 -0
- package/dist/contracts/schemas/platform-config.js.map +1 -1
- package/dist/contracts/schemas/player.d.ts +6 -0
- package/dist/contracts/schemas/player.d.ts.map +1 -1
- package/dist/contracts/schemas/player.js +9 -1
- package/dist/contracts/schemas/player.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat-moderation-expiry.int.test.d.ts +2 -0
- package/dist/engagement/chat/__tests__/chat-moderation-expiry.int.test.d.ts.map +1 -0
- package/dist/engagement/chat/__tests__/chat-moderation-expiry.int.test.js +155 -0
- package/dist/engagement/chat/__tests__/chat-moderation-expiry.int.test.js.map +1 -0
- package/dist/engagement/chat/__tests__/chat-mute-listing.int.test.d.ts +2 -0
- package/dist/engagement/chat/__tests__/chat-mute-listing.int.test.d.ts.map +1 -0
- package/dist/engagement/chat/__tests__/chat-mute-listing.int.test.js +81 -0
- package/dist/engagement/chat/__tests__/chat-mute-listing.int.test.js.map +1 -0
- package/dist/engagement/chat/drizzle/migrations/0014_chat_moderation_expiry_recorded.sql +19 -0
- package/dist/engagement/chat/drizzle/migrations/meta/0014_snapshot.json +1544 -0
- package/dist/engagement/chat/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/engagement/chat/plugin.d.ts +3 -0
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/plugin.js +31 -1
- package/dist/engagement/chat/plugin.js.map +1 -1
- package/dist/engagement/chat/schema/index.d.ts +34 -0
- package/dist/engagement/chat/schema/index.d.ts.map +1 -1
- package/dist/engagement/chat/schema/index.js +3 -0
- package/dist/engagement/chat/schema/index.js.map +1 -1
- package/dist/engagement/chat/service/chat-ban.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat-ban.service.js +7 -2
- package/dist/engagement/chat/service/chat-ban.service.js.map +1 -1
- package/dist/engagement/chat/service/chat-moderation-expiry.service.d.ts +22 -0
- package/dist/engagement/chat/service/chat-moderation-expiry.service.d.ts.map +1 -0
- package/dist/engagement/chat/service/chat-moderation-expiry.service.js +80 -0
- package/dist/engagement/chat/service/chat-moderation-expiry.service.js.map +1 -0
- package/dist/engagement/chat/service/chat-mute.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat-mute.service.js +10 -2
- package/dist/engagement/chat/service/chat-mute.service.js.map +1 -1
- package/dist/engagement/chat-commands/plugin.d.ts +3 -0
- package/dist/engagement/chat-commands/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts +3 -0
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/social/plugin.d.ts +3 -0
- package/dist/engagement/social/plugin.d.ts.map +1 -1
- package/dist/fx/exchange-rate/plugin.d.ts +3 -0
- package/dist/fx/exchange-rate/plugin.d.ts.map +1 -1
- package/dist/iam/plugin.d.ts +3 -0
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/pam/identity/contract/index.d.ts +5 -0
- package/dist/pam/identity/contract/index.d.ts.map +1 -1
- package/dist/pam/identity/plugin.d.ts +3 -0
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/identity/plugin.js +1 -0
- package/dist/pam/identity/plugin.js.map +1 -1
- package/dist/pam/identity/react/account.d.ts +2 -0
- package/dist/pam/identity/react/account.d.ts.map +1 -1
- package/dist/pam/identity/react/auth.d.ts +2 -0
- package/dist/pam/identity/react/auth.d.ts.map +1 -1
- package/dist/pam/identity/router/index.d.ts +5 -0
- package/dist/pam/identity/router/index.d.ts.map +1 -1
- package/dist/pam/identity/service/capture-timezone.service.d.ts +10 -0
- package/dist/pam/identity/service/capture-timezone.service.d.ts.map +1 -0
- package/dist/pam/identity/service/capture-timezone.service.js +21 -0
- package/dist/pam/identity/service/capture-timezone.service.js.map +1 -0
- package/dist/pam/identity/service/identity.service.d.ts.map +1 -1
- package/dist/pam/identity/service/identity.service.js +9 -0
- package/dist/pam/identity/service/identity.service.js.map +1 -1
- package/dist/pam/identity/service/phone-login.service.d.ts +4 -2
- package/dist/pam/identity/service/phone-login.service.d.ts.map +1 -1
- package/dist/pam/identity/service/phone-login.service.js +7 -2
- package/dist/pam/identity/service/phone-login.service.js.map +1 -1
- package/dist/pam/player-management/contract/index.d.ts +10 -0
- package/dist/pam/player-management/contract/index.d.ts.map +1 -1
- package/dist/pam/player-management/plugin.d.ts +3 -0
- package/dist/pam/player-management/plugin.d.ts.map +1 -1
- package/dist/pam/player-management/router/index.d.ts +8 -0
- package/dist/pam/player-management/router/index.d.ts.map +1 -1
- package/dist/pam/player-management/service/player.service.d.ts +10 -0
- package/dist/pam/player-management/service/player.service.d.ts.map +1 -1
- package/dist/pam/player-note/plugin.d.ts +3 -0
- package/dist/pam/player-note/plugin.d.ts.map +1 -1
- package/dist/pam/profile/__tests__/profile.service.int.test.js +75 -0
- package/dist/pam/profile/__tests__/profile.service.int.test.js.map +1 -1
- package/dist/pam/profile/contract/index.d.ts +5 -0
- package/dist/pam/profile/contract/index.d.ts.map +1 -1
- package/dist/pam/profile/drizzle/migrations/0005_moaning_rachel_grey.sql +2 -0
- package/dist/pam/profile/drizzle/migrations/meta/0005_snapshot.json +242 -0
- package/dist/pam/profile/drizzle/migrations/meta/_journal.json +7 -0
- package/dist/pam/profile/plugin.d.ts +3 -0
- package/dist/pam/profile/plugin.d.ts.map +1 -1
- package/dist/pam/profile/react/profile.d.ts +5 -0
- package/dist/pam/profile/react/profile.d.ts.map +1 -1
- package/dist/pam/profile/router/index.d.ts +5 -0
- package/dist/pam/profile/router/index.d.ts.map +1 -1
- package/dist/pam/profile/schema/index.d.ts +34 -0
- package/dist/pam/profile/schema/index.d.ts.map +1 -1
- package/dist/pam/profile/schema/index.js +5 -0
- package/dist/pam/profile/schema/index.js.map +1 -1
- package/dist/pam/profile/service/profile.service.d.ts +11 -0
- package/dist/pam/profile/service/profile.service.d.ts.map +1 -1
- package/dist/pam/profile/service/profile.service.js +30 -5
- package/dist/pam/profile/service/profile.service.js.map +1 -1
- package/dist/pam/shared/player-mapper.d.ts +2 -0
- package/dist/pam/shared/player-mapper.d.ts.map +1 -1
- package/dist/pam/shared/player-mapper.js +2 -0
- package/dist/pam/shared/player-mapper.js.map +1 -1
- package/dist/pam/tag/plugin.d.ts +3 -0
- package/dist/pam/tag/plugin.d.ts.map +1 -1
- package/dist/server/runtime/core-token-catalog.d.ts +3 -0
- package/dist/server/runtime/core-token-catalog.d.ts.map +1 -1
- package/dist/testing/seed-player.d.ts +2 -0
- package/dist/testing/seed-player.d.ts.map +1 -1
- package/dist/wallet/plugin.d.ts +3 -0
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../../src/contracts/schemas/player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAOzB,eAAO,MAAM,eAAe,wEAMlB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;;;EAA0B,CAAC;AAE1D,eAAO,MAAM,YAAY,0HAQf,CAAC;AACX,eAAO,MAAM,eAAe;;;;;;;;EAAuB,CAAC;AAEpD,eAAO,MAAM,SAAS,gCAAiC,CAAC;AACxD,eAAO,MAAM,aAAa;;;EAAoB,CAAC;AAC/C,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,kBAAkB,sDAAuD,CAAC;AACvF,eAAO,MAAM,qBAAqB;;;;;EAA6B,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAE/D;AAED,eAAO,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../../src/contracts/schemas/player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAOzB,eAAO,MAAM,eAAe,wEAMlB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;;;EAA0B,CAAC;AAE1D,eAAO,MAAM,YAAY,0HAQf,CAAC;AACX,eAAO,MAAM,eAAe;;;;;;;;EAAuB,CAAC;AAEpD,eAAO,MAAM,SAAS,gCAAiC,CAAC;AACxD,eAAO,MAAM,aAAa;;;EAAoB,CAAC;AAC/C,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,eAAO,MAAM,kBAAkB,sDAAuD,CAAC;AACvF,eAAO,MAAM,qBAAqB;;;;;EAA6B,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAE/D;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuBvB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKjC,CAAC;AAEH,eAAO,MAAM,qBAAqB,mHASxB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;;;;;;GAAqD,CAAC;AACrF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAK1C,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAE5F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAa,GAAG,OAAO,CAMjF;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;iBAkBvC,CAAC;AAEL,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
|
-
import { MoneyAmountSchema, TimestampSchema, UuidSchema } from './common.js';
|
|
2
|
+
import { MoneyAmountSchema, TimestampSchema, TimezoneSchema, UuidSchema } from './common.js';
|
|
3
3
|
import { CountryCodeSchema, CurrencyCodeSchema } from './igaming-config.js';
|
|
4
4
|
import { E164PhoneSchema } from './identity.js';
|
|
5
5
|
import { TagKeySchema } from './tag.js';
|
|
@@ -54,6 +54,10 @@ export const PlayerSchema = z.object({
|
|
|
54
54
|
totalWagered: MoneyAmountSchema,
|
|
55
55
|
totalDeposits: MoneyAmountSchema,
|
|
56
56
|
lastSeenAt: z.string().nullable(),
|
|
57
|
+
// The IANA zone the player's browser last reported; null until a device reports one.
|
|
58
|
+
timezone: TimezoneSchema.nullable(),
|
|
59
|
+
// Last confirmed by a device, not last changed: it moves on every accepted capture.
|
|
60
|
+
timezoneUpdatedAt: TimestampSchema.nullable(),
|
|
57
61
|
createdAt: TimestampSchema,
|
|
58
62
|
updatedAt: TimestampSchema,
|
|
59
63
|
});
|
|
@@ -98,6 +102,9 @@ export function isAdultDateOfBirth(dateOfBirth, now = new Date()) {
|
|
|
98
102
|
* phones. `null` clears a field; omitting it leaves the stored value alone. `phone` is the
|
|
99
103
|
* player's self-declared contact number - the verified login credential lives on the
|
|
100
104
|
* identity module's `user.phoneNumber` and is never written from here.
|
|
105
|
+
*
|
|
106
|
+
* `timezone` is the odd one out: not nullable (worth capturing, never worth clearing) and
|
|
107
|
+
* written by `recordTimezone` rather than by the field update.
|
|
101
108
|
*/
|
|
102
109
|
export const UpdatePlayerProfileInputSchema = z
|
|
103
110
|
.object({
|
|
@@ -112,6 +119,7 @@ export const UpdatePlayerProfileInputSchema = z
|
|
|
112
119
|
phone: E164PhoneSchema.nullable(),
|
|
113
120
|
country: CountryCodeSchema.nullable(),
|
|
114
121
|
currency: CurrencyCodeSchema,
|
|
122
|
+
timezone: TimezoneSchema,
|
|
115
123
|
})
|
|
116
124
|
.partial()
|
|
117
125
|
.refine((v) => Object.values(v).some((x) => x !== undefined), {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"player.js","sourceRoot":"","sources":["../../../src/contracts/schemas/player.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"player.js","sourceRoot":"","sources":["../../../src/contracts/schemas/player.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-moderation-expiry.int.test.d.ts","sourceRoot":"","sources":["../../../../src/engagement/chat/__tests__/chat-moderation-expiry.int.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { eq, sql } from 'drizzle-orm';
|
|
4
|
+
import { createTestDb } from '@openora/core/testing';
|
|
5
|
+
import { auditLog } from '@openora/core/audit/schema';
|
|
6
|
+
import { migrate as migrateAudit } from '@openora/core/audit/migrate';
|
|
7
|
+
import { AuditService } from '@openora/core/audit/server';
|
|
8
|
+
import { makeEventBus, makeIdentityReader, mock, NO_CLIENT_META } from '../../../testing/mock.js';
|
|
9
|
+
import { migrate } from '../migrate.js';
|
|
10
|
+
import { chatMute, chatPlatformBan } from '../schema/index.js';
|
|
11
|
+
import { ChatModerationExpiryService } from '../service/chat-moderation-expiry.service.js';
|
|
12
|
+
import { ChatModerationService } from '../service/chat-moderation.service.js';
|
|
13
|
+
let db;
|
|
14
|
+
let audit;
|
|
15
|
+
const ADMIN_ID = randomUUID();
|
|
16
|
+
const secondsFromNow = (seconds) => new Date(Date.now() + seconds * 1000);
|
|
17
|
+
async function seedMute(overrides = {}) {
|
|
18
|
+
const [row] = await db.drizzle.db
|
|
19
|
+
.insert(chatMute)
|
|
20
|
+
.values({
|
|
21
|
+
userId: randomUUID(),
|
|
22
|
+
roomId: null,
|
|
23
|
+
scope: '__global',
|
|
24
|
+
mutedBy: ADMIN_ID,
|
|
25
|
+
reason: 'spam',
|
|
26
|
+
...overrides,
|
|
27
|
+
})
|
|
28
|
+
.returning();
|
|
29
|
+
return row;
|
|
30
|
+
}
|
|
31
|
+
async function seedBan(overrides = {}) {
|
|
32
|
+
const [row] = await db.drizzle.db
|
|
33
|
+
.insert(chatPlatformBan)
|
|
34
|
+
.values({
|
|
35
|
+
userId: randomUUID(),
|
|
36
|
+
bannedBy: ADMIN_ID,
|
|
37
|
+
roomId: null,
|
|
38
|
+
scope: '__all_public',
|
|
39
|
+
reason: 'abuse',
|
|
40
|
+
...overrides,
|
|
41
|
+
})
|
|
42
|
+
.returning();
|
|
43
|
+
return row;
|
|
44
|
+
}
|
|
45
|
+
const auditRowsFor = (resourceId) => db.drizzle.db.select().from(auditLog).where(eq(auditLog.resourceId, resourceId));
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
db = await createTestDb([migrate, migrateAudit]);
|
|
48
|
+
// The real audit writer, so "exactly one audit entry" is a row count in audit_log
|
|
49
|
+
// rather than a call count on a spy - the hash chain has to accept these writes too.
|
|
50
|
+
const svc = new AuditService(db.drizzle, makeEventBus(), makeIdentityReader());
|
|
51
|
+
audit = {
|
|
52
|
+
record: (entry) => svc.record(entry).then(() => undefined),
|
|
53
|
+
recordInTransaction: (tx, entry) => svc.recordInTransaction(tx, entry).then(() => undefined),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
afterAll(async () => {
|
|
57
|
+
await db.drop();
|
|
58
|
+
});
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
await db.drizzle.db.execute(sql `TRUNCATE ${chatMute}, ${chatPlatformBan}, ${auditLog} RESTART IDENTITY CASCADE`);
|
|
61
|
+
});
|
|
62
|
+
describe('ChatModerationExpiryService.sweep', () => {
|
|
63
|
+
const makeSweep = () => new ChatModerationExpiryService(db.drizzle, audit);
|
|
64
|
+
const makeModeration = () => new ChatModerationService(db.drizzle, mock({}), audit);
|
|
65
|
+
it('records exactly one audit entry for a lapsed mute, dated from its own expiresAt', async () => {
|
|
66
|
+
const expiresAt = secondsFromNow(-30);
|
|
67
|
+
const mute = await seedMute({ expiresAt });
|
|
68
|
+
expect(await makeSweep().sweep()).toEqual({ mutes: 1, bans: 0 });
|
|
69
|
+
const rows = await auditRowsFor(mute.id);
|
|
70
|
+
expect(rows).toHaveLength(1);
|
|
71
|
+
expect(rows[0]).toMatchObject({
|
|
72
|
+
action: 'chat.mute.expired',
|
|
73
|
+
actorType: 'system',
|
|
74
|
+
actorId: null,
|
|
75
|
+
resourceType: 'chat_mute',
|
|
76
|
+
});
|
|
77
|
+
// The instant the player could post again, not the instant the cron noticed.
|
|
78
|
+
expect(rows[0].before['expiresAt']).toBe(expiresAt.toISOString());
|
|
79
|
+
expect(rows[0].before['userId']).toBe(mute.userId);
|
|
80
|
+
});
|
|
81
|
+
it('records exactly one audit entry for a lapsed platform ban', async () => {
|
|
82
|
+
const expiresAt = secondsFromNow(-30);
|
|
83
|
+
const ban = await seedBan({ expiresAt });
|
|
84
|
+
expect(await makeSweep().sweep()).toEqual({ mutes: 0, bans: 1 });
|
|
85
|
+
const rows = await auditRowsFor(ban.id);
|
|
86
|
+
expect(rows).toHaveLength(1);
|
|
87
|
+
expect(rows[0]).toMatchObject({
|
|
88
|
+
action: 'chat.platform_ban.expired',
|
|
89
|
+
actorType: 'system',
|
|
90
|
+
actorId: null,
|
|
91
|
+
resourceType: 'chat_platform_ban',
|
|
92
|
+
});
|
|
93
|
+
expect(rows[0].before['expiresAt']).toBe(expiresAt.toISOString());
|
|
94
|
+
});
|
|
95
|
+
it('is idempotent - a second sweep records nothing more', async () => {
|
|
96
|
+
const mute = await seedMute({ expiresAt: secondsFromNow(-30) });
|
|
97
|
+
const ban = await seedBan({ expiresAt: secondsFromNow(-30) });
|
|
98
|
+
await makeSweep().sweep();
|
|
99
|
+
expect(await makeSweep().sweep()).toEqual({ mutes: 0, bans: 0 });
|
|
100
|
+
expect(await auditRowsFor(mute.id)).toHaveLength(1);
|
|
101
|
+
expect(await auditRowsFor(ban.id)).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
it('leaves permanent, still-running and already-lifted entries alone', async () => {
|
|
104
|
+
await seedMute({ expiresAt: null });
|
|
105
|
+
await seedBan({ expiresAt: null });
|
|
106
|
+
await seedMute({ expiresAt: secondsFromNow(3600) });
|
|
107
|
+
await seedBan({ expiresAt: secondsFromNow(3600) });
|
|
108
|
+
// A lapsed entry an admin had already lifted: its end is on the trail as
|
|
109
|
+
// `chat.mute.lifted`, so a second entry would double-report the same ending.
|
|
110
|
+
await seedMute({ expiresAt: secondsFromNow(-30), liftedAt: new Date(), liftedBy: ADMIN_ID });
|
|
111
|
+
await seedBan({ expiresAt: secondsFromNow(-30), liftedAt: new Date(), liftedBy: ADMIN_ID });
|
|
112
|
+
expect(await makeSweep().sweep()).toEqual({ mutes: 0, bans: 0 });
|
|
113
|
+
expect(await db.drizzle.db.select().from(auditLog)).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
it('writes one entry per lapse when two sweeps run concurrently over the same rows', async () => {
|
|
116
|
+
const mute = await seedMute({ expiresAt: secondsFromNow(-30) });
|
|
117
|
+
const ban = await seedBan({ expiresAt: secondsFromNow(-30) });
|
|
118
|
+
const [a, b] = await Promise.all([makeSweep().sweep(), makeSweep().sweep()]);
|
|
119
|
+
expect(a.mutes + b.mutes).toBe(1);
|
|
120
|
+
expect(a.bans + b.bans).toBe(1);
|
|
121
|
+
expect(await auditRowsFor(mute.id)).toHaveLength(1);
|
|
122
|
+
expect(await auditRowsFor(ban.id)).toHaveLength(1);
|
|
123
|
+
});
|
|
124
|
+
it('stamps expiryRecordedAt so the row stops matching the scan', async () => {
|
|
125
|
+
const mute = await seedMute({ expiresAt: secondsFromNow(-30) });
|
|
126
|
+
await makeSweep().sweep();
|
|
127
|
+
const [row] = await db.drizzle.db.select().from(chatMute).where(eq(chatMute.id, mute.id));
|
|
128
|
+
expect(row.expiryRecordedAt).toBeInstanceOf(Date);
|
|
129
|
+
// The bookmark is not an enforcement input: the mute is still lapsed, still unlifted.
|
|
130
|
+
expect(row.liftedAt).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
// A lapsed row is not active, so lifting it is a no-op in both directions: the trail
|
|
133
|
+
// carries `expired` or `lifted` for a given row, never both.
|
|
134
|
+
it('leaves an admin lift after the sweep off the trail', async () => {
|
|
135
|
+
const userId = randomUUID();
|
|
136
|
+
const mute = await seedMute({ userId, expiresAt: secondsFromNow(-30) });
|
|
137
|
+
const ban = await seedBan({ userId, expiresAt: secondsFromNow(-30) });
|
|
138
|
+
await makeSweep().sweep();
|
|
139
|
+
const moderation = makeModeration();
|
|
140
|
+
await moderation.unmute({ userId, roomId: '__global', actorId: ADMIN_ID, ...NO_CLIENT_META });
|
|
141
|
+
await moderation.unban({
|
|
142
|
+
userId,
|
|
143
|
+
roomId: '__all_public',
|
|
144
|
+
actorId: ADMIN_ID,
|
|
145
|
+
...NO_CLIENT_META,
|
|
146
|
+
});
|
|
147
|
+
expect((await auditRowsFor(mute.id)).map((r) => r.action)).toEqual(['chat.mute.expired']);
|
|
148
|
+
expect((await auditRowsFor(ban.id)).map((r) => r.action)).toEqual([
|
|
149
|
+
'chat.platform_ban.expired',
|
|
150
|
+
]);
|
|
151
|
+
const [muteRow] = await db.drizzle.db.select().from(chatMute).where(eq(chatMute.id, mute.id));
|
|
152
|
+
expect(muteRow.liftedAt).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=chat-moderation-expiry.int.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-moderation-expiry.int.test.js","sourceRoot":"","sources":["../../../../src/engagement/chat/__tests__/chat-moderation-expiry.int.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-mute-listing.int.test.d.ts","sourceRoot":"","sources":["../../../../src/engagement/chat/__tests__/chat-mute-listing.int.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { eq, sql } from 'drizzle-orm';
|
|
4
|
+
import { createTestDb } from '@openora/core/testing';
|
|
5
|
+
import { mock, NO_CLIENT_META } from '../../../testing/mock.js';
|
|
6
|
+
import { migrate } from '../migrate.js';
|
|
7
|
+
import { chatMute } from '../schema/index.js';
|
|
8
|
+
import { ChatModerationService } from '../service/chat-moderation.service.js';
|
|
9
|
+
let db;
|
|
10
|
+
const ADMIN_ID = randomUUID();
|
|
11
|
+
const secondsFromNow = (seconds) => new Date(Date.now() + seconds * 1000);
|
|
12
|
+
const makeModeration = () => new ChatModerationService(db.drizzle, mock({}), mock({
|
|
13
|
+
record: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
recordInTransaction: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
}));
|
|
16
|
+
async function seedMute(overrides = {}) {
|
|
17
|
+
const [row] = await db.drizzle.db
|
|
18
|
+
.insert(chatMute)
|
|
19
|
+
.values({
|
|
20
|
+
userId: randomUUID(),
|
|
21
|
+
roomId: null,
|
|
22
|
+
scope: '__global',
|
|
23
|
+
mutedBy: ADMIN_ID,
|
|
24
|
+
reason: 'spam',
|
|
25
|
+
...overrides,
|
|
26
|
+
})
|
|
27
|
+
.returning();
|
|
28
|
+
return row;
|
|
29
|
+
}
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
db = await createTestDb([migrate]);
|
|
32
|
+
});
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await db.drop();
|
|
35
|
+
});
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
await db.drizzle.db.execute(sql `TRUNCATE ${chatMute} RESTART IDENTITY CASCADE`);
|
|
38
|
+
});
|
|
39
|
+
describe('ChatMuteService.listMutes expiry', () => {
|
|
40
|
+
it('omits a mute whose duration has run out but keeps permanent and running ones', async () => {
|
|
41
|
+
const permanent = await seedMute({ expiresAt: null });
|
|
42
|
+
const running = await seedMute({ expiresAt: secondsFromNow(3600) });
|
|
43
|
+
const expired = await seedMute({ expiresAt: secondsFromNow(-1) });
|
|
44
|
+
const ids = (await makeModeration().listMutes()).map((m) => m.id);
|
|
45
|
+
expect(ids).toContain(permanent.id);
|
|
46
|
+
expect(ids).toContain(running.id);
|
|
47
|
+
// The bug: enforcement already lets this player post again (assertCanSend applies the
|
|
48
|
+
// same predicate), so the listing saying "muted" is the only thing disagreeing.
|
|
49
|
+
expect(ids).not.toContain(expired.id);
|
|
50
|
+
});
|
|
51
|
+
it('omits an expired mute for the single-player listing the backoffice reads', async () => {
|
|
52
|
+
const userId = randomUUID();
|
|
53
|
+
await seedMute({ userId, expiresAt: secondsFromNow(-60) });
|
|
54
|
+
expect(await makeModeration().listMutes(userId)).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
it('still omits a lifted mute that has not yet expired', async () => {
|
|
57
|
+
const userId = randomUUID();
|
|
58
|
+
await seedMute({ userId, expiresAt: secondsFromNow(3600), liftedAt: new Date() });
|
|
59
|
+
expect(await makeModeration().listMutes(userId)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
it('agrees with assertCanSend once a timed mute has lapsed', async () => {
|
|
62
|
+
const moderation = makeModeration();
|
|
63
|
+
const userId = randomUUID();
|
|
64
|
+
await moderation.mute({
|
|
65
|
+
userId,
|
|
66
|
+
roomId: '__global',
|
|
67
|
+
durationSeconds: 1,
|
|
68
|
+
reason: 'spam',
|
|
69
|
+
actorId: ADMIN_ID,
|
|
70
|
+
...NO_CLIENT_META,
|
|
71
|
+
});
|
|
72
|
+
expect(await moderation.listMutes(userId)).toHaveLength(1);
|
|
73
|
+
await db.drizzle.db
|
|
74
|
+
.update(chatMute)
|
|
75
|
+
.set({ expiresAt: secondsFromNow(-1) })
|
|
76
|
+
.where(eq(chatMute.userId, userId));
|
|
77
|
+
await expect(moderation.assertCanSend(userId, null)).resolves.not.toThrow();
|
|
78
|
+
expect(await moderation.listMutes(userId)).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=chat-mute-listing.int.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-mute-listing.int.test.js","sourceRoot":"","sources":["../../../../src/engagement/chat/__tests__/chat-mute-listing.int.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
ALTER TABLE "chat_mute" ADD COLUMN "expiry_recorded_at" timestamp with time zone;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "chat_platform_ban" ADD COLUMN "expiry_recorded_at" timestamp with time zone;--> statement-breakpoint
|
|
3
|
+
CREATE INDEX "chat_platform_ban_expires_at_idx" ON "chat_platform_ban" USING btree ("expires_at");--> statement-breakpoint
|
|
4
|
+
-- Backfill: every mute/ban that had already lapsed before this release is stamped as
|
|
5
|
+
-- recorded without an audit entry being written for it. The sweep only ever writes an
|
|
6
|
+
-- entry it can date from the row's own expires_at, and for these rows that date is in
|
|
7
|
+
-- the past of a deployment that had no expiry trail at all - so the alternative is a
|
|
8
|
+
-- first cron tick emitting one entry per lapse the operator has ever issued. The trail
|
|
9
|
+
-- starts at this release instead; nothing that was traceable before becomes less so.
|
|
10
|
+
UPDATE "chat_mute"
|
|
11
|
+
SET "expiry_recorded_at" = "expires_at"
|
|
12
|
+
WHERE "lifted_at" IS NULL
|
|
13
|
+
AND "expires_at" IS NOT NULL
|
|
14
|
+
AND "expires_at" <= now();--> statement-breakpoint
|
|
15
|
+
UPDATE "chat_platform_ban"
|
|
16
|
+
SET "expiry_recorded_at" = "expires_at"
|
|
17
|
+
WHERE "lifted_at" IS NULL
|
|
18
|
+
AND "expires_at" IS NOT NULL
|
|
19
|
+
AND "expires_at" <= now();
|