summaryrefslogtreecommitdiff
path: root/packages/backend/test
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-11-08 15:52:37 +0000
committerdakkar <dakkar@thenautilus.net>2024-11-08 15:52:37 +0000
commitf079edaf3ccc1fea9242f0f8522ebbfc7e8242e4 (patch)
treeead184cf29c147bc74ed92ce905b46e5e42209c1 /packages/backend/test
parentmerge: Bump version number (!735) (diff)
parentRelease: 2024.10.1 (diff)
downloadsharkey-f079edaf3ccc1fea9242f0f8522ebbfc7e8242e4.tar.gz
sharkey-f079edaf3ccc1fea9242f0f8522ebbfc7e8242e4.tar.bz2
sharkey-f079edaf3ccc1fea9242f0f8522ebbfc7e8242e4.zip
Merge tag '2024.10.1' into feature/2024.10
Diffstat (limited to 'packages/backend/test')
-rw-r--r--packages/backend/test/e2e/2fa.ts98
-rw-r--r--packages/backend/test/e2e/endpoints.ts8
-rw-r--r--packages/backend/test/e2e/synalio/abuse-report.ts6
-rw-r--r--packages/backend/test/e2e/users.ts15
-rw-r--r--packages/backend/test/unit/AbuseReportNotificationService.ts6
-rw-r--r--packages/backend/test/unit/FetchInstanceMetadataService.ts20
-rw-r--r--packages/backend/test/unit/FlashService.ts152
-rw-r--r--packages/backend/test/unit/RoleService.ts150
-rw-r--r--packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts379
9 files changed, 737 insertions, 97 deletions
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 48da6ba27f..289359a2ce 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -138,13 +138,7 @@ describe('2要素認証', () => {
keyName: string,
credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
- }): {
- username: string,
- password: string,
- credential: AuthenticationResponseJSON,
- 'g-recaptcha-response'?: string | null,
- 'hcaptcha-response'?: string | null,
- } => {
+ }): misskey.entities.SigninFlowRequest => {
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([
@@ -204,17 +198,21 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const usersShowResponse = await api('users/show', {
- username,
- }, alice);
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
+ const signinWithoutTokenResponse = await api('signin-flow', {
+ ...signinParam(),
+ });
+ assert.strictEqual(signinWithoutTokenResponse.status, 200);
+ assert.deepStrictEqual(signinWithoutTokenResponse.body, {
+ finished: false,
+ next: 'totp',
+ });
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
@@ -255,27 +253,23 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
-
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
- assert.strictEqual(signinResponse.body.i, undefined);
- assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
- assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
- assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
+ assert.strictEqual(signinResponse.body.finished, false);
+ assert.strictEqual(signinResponse.body.next, 'passkey');
+ assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+ assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
+ assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
- const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
+ const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
keyName,
credentialId,
- requestOptions: signinResponse.body,
- } as any));
+ requestOptions: signinResponse.body.authRequest,
+ }));
assert.strictEqual(signinResponse2.status, 200);
+ assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
@@ -317,28 +311,30 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
+ const iResponse = await api('i', {}, alice);
+ assert.strictEqual(iResponse.status, 200);
+ assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
- assert.strictEqual(signinResponse.body.i, undefined);
+ assert.strictEqual(signinResponse.body.finished, false);
+ assert.strictEqual(signinResponse.body.next, 'passkey');
+ assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+ assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
- const signinResponse2 = await api('signin', {
+ const signinResponse2 = await api('signin-flow', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
- requestOptions: signinResponse.body,
+ requestOptions: signinResponse.body.authRequest,
} as any),
password: '',
});
assert.strictEqual(signinResponse2.status, 200);
+ assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
@@ -426,11 +422,11 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.status, 200);
// テストの実行順によっては複数残ってるので全部消す
- const iResponse = await api('i', {
+ const beforeIResponse = await api('i', {
}, alice);
- assert.strictEqual(iResponse.status, 200);
- assert.ok(iResponse.body.securityKeysList);
- for (const key of iResponse.body.securityKeysList) {
+ assert.strictEqual(beforeIResponse.status, 200);
+ assert.ok(beforeIResponse.body.securityKeysList);
+ for (const key of beforeIResponse.body.securityKeysList) {
const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
@@ -439,17 +435,16 @@ describe('2要素認証', () => {
assert.strictEqual(removeKeyResponse.status, 200);
}
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
+ const afterIResponse = await api('i', {}, alice);
+ assert.strictEqual(afterIResponse.status, 200);
+ assert.strictEqual(afterIResponse.body.securityKeys, false);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
@@ -470,11 +465,9 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
+ const iResponse = await api('i', {}, alice);
+ assert.strictEqual(iResponse.status, 200);
+ assert.strictEqual(iResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
@@ -482,10 +475,11 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index 5aaec7f6f9..b91d77c398 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -66,9 +66,9 @@ describe('Endpoints', () => {
});
});
- describe('signin', () => {
+ describe('signin-flow', () => {
test('間違ったパスワードでサインインできない', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
password: 'bar',
});
@@ -77,7 +77,7 @@ describe('Endpoints', () => {
});
test('クエリをインジェクションできない', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
// @ts-expect-error password must be string
password: {
@@ -89,7 +89,7 @@ describe('Endpoints', () => {
});
test('正しい情報でサインインできる', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
password: 'test1',
});
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index 6ce6e47781..c98d199f35 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
});
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
});
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 7d2e14f85d..7b21834b57 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -86,9 +86,6 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
- twoFactorEnabled: user.twoFactorEnabled,
- usePasswordLessLogin: user.usePasswordLessLogin,
- securityKeys: user.securityKeys,
roles: user.roles,
memo: user.memo,
});
@@ -153,6 +150,9 @@ describe('ユーザー', () => {
achievements: user.achievements,
loggedInDays: user.loggedInDays,
policies: user.policies,
+ twoFactorEnabled: user.twoFactorEnabled,
+ usePasswordLessLogin: user.usePasswordLessLogin,
+ securityKeys: user.securityKeys,
...(security ? {
email: user.email,
emailVerified: user.emailVerified,
@@ -350,9 +350,6 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
- assert.strictEqual(response.twoFactorEnabled, false);
- assert.strictEqual(response.usePasswordLessLogin, false);
- assert.strictEqual(response.securityKeys, false);
assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null);
@@ -393,6 +390,9 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
+ assert.strictEqual(response.twoFactorEnabled, false);
+ assert.strictEqual(response.usePasswordLessLogin, false);
+ assert.strictEqual(response.securityKeys, false);
assert.notStrictEqual(response.email, undefined);
assert.strictEqual(response.emailVerified, false);
assert.deepStrictEqual(response.securityKeysList, []);
@@ -649,6 +649,9 @@ describe('ユーザー', () => {
{ label: '自分以外から見たときはAdministratorか判定できない', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
{ label: 'Moderatorになっている', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
{ label: '自分以外から見たときはModeratorか判定できない', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
+ { label: '自分から見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false },
+ { label: '自分以外から見た場合に二要素認証関連のプロパティがセットされていない', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined },
+ { label: 'モデレーターから見た場合に二要素認証関連のプロパティがセットされている', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false },
{ label: 'サイレンスになっている', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
// FIXME: 落ちる
//{ label: 'サスペンドになっている', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
index e971659070..235af29f0d 100644
--- a/packages/backend/test/unit/AbuseReportNotificationService.ts
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -5,6 +5,7 @@
import { jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import {
AbuseReportNotificationRecipientRepository,
@@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
-import { randomString } from '../utils.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
process.env.NODE_ENV = 'test';
@@ -111,6 +112,9 @@ describe('AbuseReportNotificationService', () => {
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
},
{
+ provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
+ },
+ {
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
},
{
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index bf8f3ab0e3..1e3605aafc 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -8,6 +8,7 @@ process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
import { Test } from '@nestjs/testing';
import { Redis } from 'ioredis';
+import type { TestingModule } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -16,7 +17,6 @@ import { LoggerService } from '@/core/LoggerService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
-import type { TestingModule } from '@nestjs/testing';
function mockRedis() {
const hash = {} as any;
@@ -52,7 +52,7 @@ describe('FetchInstanceMetadataService', () => {
if (token === HttpRequestService) {
return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() };
} else if (token === FederatedInstanceService) {
- return { fetch: jest.fn() };
+ return { fetchOrRegister: jest.fn() };
} else if (token === DI.redis) {
return mockRedis;
}
@@ -75,7 +75,7 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -83,14 +83,14 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
test('Lock and don\'t update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -98,14 +98,14 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -114,14 +114,14 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
test('Do when lock not acquired but forced', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -130,7 +130,7 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
expect(unlockSpy).toHaveBeenCalledTimes(1);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
});
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+ let app: TestingModule;
+ let service: FlashService;
+
+ // --------------------------------------------------------------------------------------
+
+ let flashsRepository: FlashsRepository;
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+ let alice: MiUser;
+ let bob: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createFlash(data: Partial<MiFlash>) {
+ return flashsRepository.insert({
+ id: idService.gen(),
+ updatedAt: new Date(),
+ userId: root.id,
+ title: 'title',
+ summary: 'summary',
+ script: 'script',
+ permissions: [],
+ likedCount: 0,
+ ...data,
+ }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createUser(data: Partial<MiUser> = {}) {
+ const user = await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeEach(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ FlashService,
+ IdService,
+ ],
+ }).compile();
+
+ service = app.get(FlashService);
+
+ flashsRepository = app.get(DI.flashsRepository);
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+ idService = app.get(IdService);
+
+ root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+ });
+
+ afterEach(async () => {
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ await flashsRepository.delete({});
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('featured', () => {
+ test('should return featured flashes', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash3, flash2, flash1]);
+ });
+
+ test('should return featured flashes public visibility only', async () => {
+ const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+ const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+ const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with offset', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 1,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with limit', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 2,
+ });
+
+ expect(result).toEqual([flash3, flash2]);
+ });
+ });
+});
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index ef80d25f81..9c1b1008d6 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -10,6 +10,8 @@ import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
+import type { TestingModule } from '@nestjs/testing';
+import type { MockFunctionMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
@@ -31,8 +33,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
@@ -277,9 +277,9 @@ describe('RoleService', () => {
});
describe('getModeratorIds', () => {
- test('includeAdmins = false, excludeExpire = false', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -295,13 +295,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(false, false);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: false,
+ excludeExpire: false,
+ });
expect(result).toEqual([modeUser1.id, modeUser2.id]);
});
- test('includeAdmins = false, excludeExpire = true', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -317,13 +321,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(false, true);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: false,
+ excludeExpire: true,
+ });
expect(result).toEqual([modeUser1.id]);
});
- test('includeAdmins = true, excludeExpire = false', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -339,13 +347,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(true, false);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: true,
+ includeRoot: false,
+ excludeExpire: false,
+ });
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
});
- test('includeAdmins = true, excludeExpire = true', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -361,9 +373,111 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(true, true);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: true,
+ includeRoot: false,
+ excludeExpire: true,
+ });
expect(result).toEqual([adminUser1.id, modeUser1.id]);
});
+
+ test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: true,
+ excludeExpire: false,
+ });
+ expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
+ });
+
+ test('root has moderator role', async () => {
+ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: rootUser.id, roleId: role2.id }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: true,
+ excludeExpire: false,
+ });
+ expect(result).toEqual([modeUser1.id, rootUser.id]);
+ });
+
+ test('root has administrator role', async () => {
+ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: rootUser.id, roleId: role1.id }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: true,
+ includeRoot: true,
+ excludeExpire: false,
+ });
+ expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
+ });
+
+ test('root has moderator role(expire)', async () => {
+ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: true,
+ excludeExpire: true,
+ });
+ expect(result).toEqual([rootUser.id]);
+ });
});
describe('conditional role', () => {
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..1506283a3c
--- /dev/null
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,379 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import * as lolex from '@sinonjs/fake-timers';
+import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
+import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MetaService } from '@/core/MetaService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+
+const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
+
+describe('CheckModeratorsActivityProcessorService', () => {
+ let app: TestingModule;
+ let clock: lolex.InstalledClock;
+ let service: CheckModeratorsActivityProcessorService;
+
+ // --------------------------------------------------------------------------------------
+
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+ let roleService: jest.Mocked<RoleService>;
+ let announcementService: jest.Mocked<AnnouncementService>;
+ let emailService: jest.Mocked<EmailService>;
+ let systemWebhookService: jest.Mocked<SystemWebhookService>;
+
+ let systemWebhook1: MiSystemWebhook;
+ let systemWebhook2: MiSystemWebhook;
+ let systemWebhook3: MiSystemWebhook;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createUser(data: Partial<MiUser> = {}, profile: Partial<MiUserProfile> = {}): Promise<MiUser> {
+ const id = idService.gen();
+ const user = await usersRepository
+ .insert({
+ id: id,
+ username: `user_${id}`,
+ usernameLower: `user_${id}`.toLowerCase(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ ...profile,
+ });
+
+ return user;
+ }
+
+ function crateSystemWebhook(data: Partial<MiSystemWebhook> = {}): MiSystemWebhook {
+ return {
+ id: idService.gen(),
+ isActive: true,
+ updatedAt: new Date(),
+ latestSentAt: null,
+ latestStatus: null,
+ name: 'test',
+ url: 'https://example.com',
+ secret: 'test',
+ on: [],
+ ...data,
+ };
+ }
+
+ function mockModeratorRole(users: MiUser[]) {
+ roleService.getModerators.mockReset();
+ roleService.getModerators.mockResolvedValue(users);
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeAll(async () => {
+ app = await Test
+ .createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ CheckModeratorsActivityProcessorService,
+ IdService,
+ {
+ provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
+ },
+ {
+ provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+ },
+ {
+ provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
+ },
+ {
+ provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+ },
+ {
+ provide: SystemWebhookService, useFactory: () => ({
+ fetchActiveSystemWebhooks: jest.fn(),
+ enqueueSystemWebhook: jest.fn(),
+ }),
+ },
+ {
+ provide: QueueLoggerService, useFactory: () => ({
+ logger: ({
+ createSubLogger: () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ succ: jest.fn(),
+ }),
+ }),
+ }),
+ },
+ ],
+ })
+ .compile();
+
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+
+ service = app.get(CheckModeratorsActivityProcessorService);
+ idService = app.get(IdService);
+ roleService = app.get(RoleService) as jest.Mocked<RoleService>;
+ announcementService = app.get(AnnouncementService) as jest.Mocked<AnnouncementService>;
+ emailService = app.get(EmailService) as jest.Mocked<EmailService>;
+ systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
+
+ app.enableShutdownHooks();
+ });
+
+ beforeEach(async () => {
+ clock = lolex.install({
+ now: new Date(baseDate),
+ shouldClearNativeTimers: true,
+ });
+
+ systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] });
+ systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] });
+ systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] });
+
+ emailService.sendEmail.mockReturnValue(Promise.resolve());
+ announcementService.create.mockReturnValue(Promise.resolve({} as never));
+ systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]);
+ systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never));
+ });
+
+ afterEach(async () => {
+ clock.uninstall();
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ roleService.getModerators.mockReset();
+ announcementService.create.mockReset();
+ emailService.sendEmail.mockReset();
+ systemWebhookService.enqueueSystemWebhook.mockReset();
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('evaluateModeratorsInactiveDays', () => {
+ test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => {
+ const [user1, user2, user3, user4] = await Promise.all([
+ // 期限よりも1秒新しいタイミングでアクティブ化(セーフ)
+ createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }),
+ // 期限ちょうどにアクティブ化(セーフ)
+ createUser({ lastActiveDate: subDays(baseDate, 7) }),
+ // 期限よりも1秒古いタイミングでアクティブ化(アウト)
+ createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+ // 対象外
+ createUser({ lastActiveDate: null }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, user4]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user3]);
+ });
+
+ test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => {
+ const [user1, user2] = await Promise.all([
+ // 期限よりも1秒古いタイミングでアクティブ化(アウト)
+ createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+ // 対象外
+ createUser({ lastActiveDate: null }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1]);
+ });
+
+ test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限まで残り24時間->猶予1日として計算されるはずである
+ createUser({ lastActiveDate: subDays(baseDate, 6) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(24);
+ });
+
+ test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限まで残り25時間->猶予1日として計算されるはずである
+ createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(25);
+ });
+
+ test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限まで残り23時間->猶予0日として計算されるはずである
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(23);
+ });
+
+ test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限ちょうど->猶予0日として計算されるはずである
+ createUser({ lastActiveDate: subDays(baseDate, 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(0);
+ });
+
+ test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限より1時間超過->猶予-1日として計算されるはずである
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1, user2]);
+ expect(result.remainingTime.asDays).toBe(-1);
+ expect(result.remainingTime.asHours).toBe(-1);
+ });
+
+ test('[remainingTime] 期限より25時間超過している場合、猶予-2日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 10) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限より1時間超過->猶予-1日として計算されるはずである
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1, user2]);
+ expect(result.remainingTime.asDays).toBe(-2);
+ expect(result.remainingTime.asHours).toBe(-25);
+ });
+ });
+
+ describe('notifyInactiveModeratorsWarning', () => {
+ test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+ const [user1, user2, user3, user4, root] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ createUser({}, { email: 'user2@example.com', emailVerified: false }),
+ createUser({}, { email: null, emailVerified: false }),
+ createUser({}, { email: 'user4@example.com', emailVerified: true }),
+ createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, root]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+ expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+ });
+
+ test('[systemWebhook] "inactiveModeratorsWarning"が有効なSystemWebhookに対して送信される', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+ });
+ });
+
+ describe('notifyChangeToInvitationOnly', () => {
+ test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => {
+ const [user1, user2, user3, user4, root] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ createUser({}, { email: 'user2@example.com', emailVerified: false }),
+ createUser({}, { email: null, emailVerified: false }),
+ createUser({}, { email: 'user4@example.com', emailVerified: true }),
+ createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, root]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(announcementService.create).toHaveBeenCalledTimes(4);
+ expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id);
+ expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id);
+ expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id);
+ expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id);
+
+ expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+ expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+ });
+
+ test('[systemWebhook] "inactiveModeratorsInvitationOnlyChanged"が有効なSystemWebhookに対して送信される', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
+ });
+ });
+});