From 33b34ad7b8248b4d5ddc37b986ffcf4dff6a37c4 Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Sun, 13 Oct 2024 20:32:12 +0900 Subject: feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知 (#14757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知 * fix misskey-js.api.md * Revert "feat: 運営のアクティビティが一定期間ない場合は通知+招待制に移行した際に通知" This reverts commit 3ab953bdf87f28411a1a10bce787a23d238cda80. * 通知をやめてユーザ単位でのお知らせ機能に変更 * テスト用実装を戻す * Update packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> * fix remove empty then --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- .../CheckModeratorsActivityProcessorService.ts | 168 +++++++++++++++++++-- 1 file changed, 156 insertions(+), 12 deletions(-) (limited to 'packages/backend/test/unit/queue') diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts index b783320aa0..1506283a3c 100644 --- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -8,13 +8,16 @@ 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 { MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.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)); @@ -29,10 +32,17 @@ describe('CheckModeratorsActivityProcessorService', () => { let userProfilesRepository: UserProfilesRepository; let idService: IdService; let roleService: jest.Mocked; + let announcementService: jest.Mocked; + let emailService: jest.Mocked; + let systemWebhookService: jest.Mocked; + + let systemWebhook1: MiSystemWebhook; + let systemWebhook2: MiSystemWebhook; + let systemWebhook3: MiSystemWebhook; // -------------------------------------------------------------------------------------- - async function createUser(data: Partial = {}) { + async function createUser(data: Partial = {}, profile: Partial = {}): Promise { const id = idService.gen(); const user = await usersRepository .insert({ @@ -45,11 +55,27 @@ describe('CheckModeratorsActivityProcessorService', () => { await userProfilesRepository.insert({ userId: user.id, + ...profile, }); return user; } + function crateSystemWebhook(data: Partial = {}): 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); @@ -72,6 +98,18 @@ describe('CheckModeratorsActivityProcessorService', () => { { 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: ({ @@ -93,6 +131,9 @@ describe('CheckModeratorsActivityProcessorService', () => { service = app.get(CheckModeratorsActivityProcessorService); idService = app.get(IdService); roleService = app.get(RoleService) as jest.Mocked; + announcementService = app.get(AnnouncementService) as jest.Mocked; + emailService = app.get(EmailService) as jest.Mocked; + systemWebhookService = app.get(SystemWebhookService) as jest.Mocked; app.enableShutdownHooks(); }); @@ -102,6 +143,15 @@ describe('CheckModeratorsActivityProcessorService', () => { 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 () => { @@ -109,6 +159,9 @@ describe('CheckModeratorsActivityProcessorService', () => { await usersRepository.delete({}); await userProfilesRepository.delete({}); roleService.getModerators.mockReset(); + announcementService.create.mockReset(); + emailService.sendEmail.mockReset(); + systemWebhookService.enqueueSystemWebhook.mockReset(); }); afterAll(async () => { @@ -152,7 +205,7 @@ describe('CheckModeratorsActivityProcessorService', () => { expect(result.inactiveModerators).toEqual([user1]); }); - test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => { + test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => { const [user1, user2] = await Promise.all([ createUser({ lastActiveDate: subDays(baseDate, 8) }), // 猶予はこのユーザ基準で計算される想定。 @@ -165,10 +218,11 @@ describe('CheckModeratorsActivityProcessorService', () => { const result = await service.evaluateModeratorsInactiveDays(); expect(result.isModeratorsInactive).toBe(false); expect(result.inactiveModerators).toEqual([user1]); - expect(result.inactivityLimitCountdown).toBe(1); + expect(result.remainingTime.asDays).toBe(1); + expect(result.remainingTime.asHours).toBe(24); }); - test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => { + test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => { const [user1, user2] = await Promise.all([ createUser({ lastActiveDate: subDays(baseDate, 8) }), // 猶予はこのユーザ基準で計算される想定。 @@ -181,10 +235,11 @@ describe('CheckModeratorsActivityProcessorService', () => { const result = await service.evaluateModeratorsInactiveDays(); expect(result.isModeratorsInactive).toBe(false); expect(result.inactiveModerators).toEqual([user1]); - expect(result.inactivityLimitCountdown).toBe(1); + expect(result.remainingTime.asDays).toBe(1); + expect(result.remainingTime.asHours).toBe(25); }); - test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => { + test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => { const [user1, user2] = await Promise.all([ createUser({ lastActiveDate: subDays(baseDate, 8) }), // 猶予はこのユーザ基準で計算される想定。 @@ -197,10 +252,11 @@ describe('CheckModeratorsActivityProcessorService', () => { const result = await service.evaluateModeratorsInactiveDays(); expect(result.isModeratorsInactive).toBe(false); expect(result.inactiveModerators).toEqual([user1]); - expect(result.inactivityLimitCountdown).toBe(0); + expect(result.remainingTime.asDays).toBe(0); + expect(result.remainingTime.asHours).toBe(23); }); - test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => { + test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => { const [user1, user2] = await Promise.all([ createUser({ lastActiveDate: subDays(baseDate, 8) }), // 猶予はこのユーザ基準で計算される想定。 @@ -213,10 +269,11 @@ describe('CheckModeratorsActivityProcessorService', () => { const result = await service.evaluateModeratorsInactiveDays(); expect(result.isModeratorsInactive).toBe(false); expect(result.inactiveModerators).toEqual([user1]); - expect(result.inactivityLimitCountdown).toBe(0); + expect(result.remainingTime.asDays).toBe(0); + expect(result.remainingTime.asHours).toBe(0); }); - test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => { + test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => { const [user1, user2] = await Promise.all([ createUser({ lastActiveDate: subDays(baseDate, 8) }), // 猶予はこのユーザ基準で計算される想定。 @@ -229,7 +286,94 @@ describe('CheckModeratorsActivityProcessorService', () => { const result = await service.evaluateModeratorsInactiveDays(); expect(result.isModeratorsInactive).toBe(true); expect(result.inactiveModerators).toEqual([user1, user2]); - expect(result.inactivityLimitCountdown).toBe(-1); + 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); }); }); }); -- cgit v1.2.3-freya