mizzkey/packages/backend/test/unit/SystemWebhookService.ts
おさむのひと 61fae45390
feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする (#13758)
* feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする

* モデログに対応&エンドポイントを単一オブジェクトでのサポートに変更(API経由で大量に作るシチュエーションもないと思うので)

* fix spdx

* fix migration

* fix migration

* fix models

* add e2e webhook

* tweak

* fix modlog

* fix bugs

* add tests and fix bugs

* add tests and fix bugs

* add tests

* fix path

* regenerate locale

* 混入除去

* 混入除去

* add abuseReportResolved

* fix pnpm-lock.yaml

* add abuseReportResolved test

* fix bugs

* fix ui

* add tests

* fix CHANGELOG.md

* add tests

* add RoleService.getModeratorIds tests

* WebhookServiceをUserとSystemに分割

* fix CHANGELOG.md

* fix test

* insertOneを使う用に

* fix

* regenerate locales

* revert version

* separate webhook job queue

* fix

* 🎨

* Update QueueProcessorService.ts

---------

Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-06-08 15:34:19 +09:00

516 lines
15 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { MiUser } from '@/models/User.js';
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { randomString, sleep } from '../utils.js';
describe('SystemWebhookService', () => {
let app: TestingModule;
let service: SystemWebhookService;
// --------------------------------------------------------------------------------------
let usersRepository: UsersRepository;
let systemWebhooksRepository: SystemWebhooksRepository;
let idService: IdService;
let queueService: jest.Mocked<QueueService>;
// --------------------------------------------------------------------------------------
let root: MiUser;
// --------------------------------------------------------------------------------------
async function createUser(data: Partial<MiUser> = {}) {
return await usersRepository
.insert({
id: idService.gen(),
...data,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
}
async function createWebhook(data: Partial<MiSystemWebhook> = {}) {
return systemWebhooksRepository
.insert({
id: idService.gen(),
name: randomString(),
on: ['abuseReport'],
url: 'https://example.com',
secret: randomString(),
...data,
})
.then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0]));
}
// --------------------------------------------------------------------------------------
async function beforeAllImpl() {
app = await Test
.createTestingModule({
imports: [
GlobalModule,
],
providers: [
SystemWebhookService,
IdService,
LoggerService,
GlobalEventService,
{
provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
},
{
provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }),
},
],
})
.compile();
usersRepository = app.get(DI.usersRepository);
systemWebhooksRepository = app.get(DI.systemWebhooksRepository);
service = app.get(SystemWebhookService);
idService = app.get(IdService);
queueService = app.get(QueueService) as jest.Mocked<QueueService>;
app.enableShutdownHooks();
}
async function afterAllImpl() {
await app.close();
}
async function beforeEachImpl() {
root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
}
async function afterEachImpl() {
await usersRepository.delete({});
await systemWebhooksRepository.delete({});
}
// --------------------------------------------------------------------------------------
describe('アプリを毎回作り直す必要のないグループ', () => {
beforeAll(beforeAllImpl);
afterAll(afterAllImpl);
beforeEach(beforeEachImpl);
afterEach(afterEachImpl);
describe('fetchSystemWebhooks', () => {
test('フィルタなし', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook3 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook4 = await createWebhook({
isActive: false,
on: [],
});
const fetchedWebhooks = await service.fetchSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
});
test('activeのみ', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook3 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook4 = await createWebhook({
isActive: false,
on: [],
});
const fetchedWebhooks = await service.fetchSystemWebhooks({ isActive: true });
expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
});
test('特定のイベントのみ', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook3 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook4 = await createWebhook({
isActive: false,
on: [],
});
const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'] });
expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
});
test('activeな特定のイベントのみ', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook3 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook4 = await createWebhook({
isActive: false,
on: [],
});
const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'], isActive: true });
expect(fetchedWebhooks).toEqual([webhook1]);
});
test('ID指定', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook3 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook4 = await createWebhook({
isActive: false,
on: [],
});
const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id] });
expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
});
test('ID指定(他条件とANDになるか見たい)', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook3 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook4 = await createWebhook({
isActive: false,
on: [],
});
const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
expect(fetchedWebhooks).toEqual([webhook4]);
});
});
describe('createSystemWebhook', () => {
test('作成成功 ', async () => {
const params = {
isActive: true,
name: randomString(),
on: ['abuseReport'] as SystemWebhookEventType[],
url: 'https://example.com',
secret: randomString(),
};
const webhook = await service.createSystemWebhook(params, root);
expect(webhook).toMatchObject(params);
});
});
describe('updateSystemWebhook', () => {
test('更新成功', async () => {
const webhook = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const params = {
id: webhook.id,
isActive: false,
name: randomString(),
on: ['abuseReport'] as SystemWebhookEventType[],
url: randomString(),
secret: randomString(),
};
const updatedWebhook = await service.updateSystemWebhook(params, root);
expect(updatedWebhook).toMatchObject(params);
});
});
describe('deleteSystemWebhook', () => {
test('削除成功', async () => {
const webhook = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
await service.deleteSystemWebhook(webhook.id, root);
await expect(systemWebhooksRepository.findOneBy({ id: webhook.id })).resolves.toBeNull();
});
});
});
describe('アプリを毎回作り直す必要があるグループ', () => {
beforeEach(async () => {
await beforeAllImpl();
await beforeEachImpl();
});
afterEach(async () => {
await afterEachImpl();
await afterAllImpl();
});
describe('enqueueSystemWebhook', () => {
test('キューに追加成功', async () => {
const webhook = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
});
test('非アクティブなWebhookはキューに追加されない', async () => {
const webhook = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: [],
});
const webhook2 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
});
describe('fetchActiveSystemWebhooks', () => {
describe('systemWebhookCreated', () => {
test('ActiveなWebhookが追加された時、キャッシュに追加されている', async () => {
const webhook = await service.createSystemWebhook(
{
isActive: true,
name: randomString(),
on: ['abuseReport'],
url: 'https://example.com',
secret: randomString(),
},
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook]);
});
test('NotActiveなWebhookが追加された時、キャッシュに追加されていない', async () => {
const webhook = await service.createSystemWebhook(
{
isActive: false,
name: randomString(),
on: ['abuseReport'],
url: 'https://example.com',
secret: randomString(),
},
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([]);
});
});
describe('systemWebhookUpdated', () => {
test('ActiveなWebhookが編集された時、キャッシュに反映されている', async () => {
const id = idService.gen();
await createWebhook({ id });
// キャッシュ作成
const webhook1 = await service.fetchActiveSystemWebhooks();
// 読み込まれていることをチェック
expect(webhook1.length).toEqual(1);
expect(webhook1[0].id).toEqual(id);
const webhook2 = await service.updateSystemWebhook(
{
id,
isActive: true,
name: randomString(),
on: ['abuseReport'],
url: 'https://example.com',
secret: randomString(),
},
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook2]);
});
test('NotActiveなWebhookが編集された時、キャッシュに追加されない', async () => {
const id = idService.gen();
await createWebhook({ id, isActive: false });
// キャッシュ作成
const webhook1 = await service.fetchActiveSystemWebhooks();
// 読み込まれていないことをチェック
expect(webhook1.length).toEqual(0);
const webhook2 = await service.updateSystemWebhook(
{
id,
isActive: false,
name: randomString(),
on: ['abuseReport'],
url: 'https://example.com',
secret: randomString(),
},
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0);
});
test('NotActiveなWebhookがActiveにされた時、キャッシュに追加されている', async () => {
const id = idService.gen();
const baseWebhook = await createWebhook({ id, isActive: false });
// キャッシュ作成
const webhook1 = await service.fetchActiveSystemWebhooks();
// 読み込まれていないことをチェック
expect(webhook1.length).toEqual(0);
const webhook2 = await service.updateSystemWebhook(
{
...baseWebhook,
isActive: true,
},
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook2]);
});
test('ActiveなWebhookがNotActiveにされた時、キャッシュから削除されている', async () => {
const id = idService.gen();
const baseWebhook = await createWebhook({ id, isActive: true });
// キャッシュ作成
const webhook1 = await service.fetchActiveSystemWebhooks();
// 読み込まれていることをチェック
expect(webhook1.length).toEqual(1);
expect(webhook1[0].id).toEqual(id);
const webhook2 = await service.updateSystemWebhook(
{
...baseWebhook,
isActive: false,
},
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0);
});
});
describe('systemWebhookDeleted', () => {
test('キャッシュから削除されている', async () => {
const id = idService.gen();
const baseWebhook = await createWebhook({ id, isActive: true });
// キャッシュ作成
const webhook1 = await service.fetchActiveSystemWebhooks();
// 読み込まれていることをチェック
expect(webhook1.length).toEqual(1);
expect(webhook1[0].id).toEqual(id);
const webhook2 = await service.deleteSystemWebhook(
id,
root,
);
// redisでの配信経由で更新されるのでちょっと待つ
await sleep(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0);
});
});
});
});
});