mizzkey/packages/backend/test/e2e/synalio/abuse-report.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

402 lines
12 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { entities } from 'misskey-js';
import { beforeEach, describe, test } from '@jest/globals';
import Fastify from 'fastify';
import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
const WEBHOOK_HOST = 'http://localhost:15080';
const WEBHOOK_PORT = 15080;
process.env.NODE_ENV = 'test';
describe('[シナリオ] ユーザ通報', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;
let alice: entities.SignupResponse;
let bob: entities.SignupResponse;
type SystemWebhookPayload = {
server: string;
hookId: string;
eventId: string;
createdAt: string;
type: string;
body: any;
}
// -------------------------------------------------------------------------------------------
async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>): Promise<T> {
const fastify = Fastify();
let timeoutHandle: NodeJS.Timeout | null = null;
const result = await new Promise<string>(async (resolve, reject) => {
fastify.all('/', async (req, res) => {
timeoutHandle && clearTimeout(timeoutHandle);
const body = JSON.stringify(req.body);
res.status(200).send('ok');
await fastify.close();
resolve(body);
});
await fastify.listen({ port: WEBHOOK_PORT });
timeoutHandle = setTimeout(async () => {
await fastify.close();
reject(new Error('timeout'));
}, 3000);
try {
await postAction();
} catch (e) {
await fastify.close();
reject(e);
}
});
await fastify.close();
return JSON.parse(result) as T;
}
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
const res = await api(
'admin/system-webhook/create',
{
isActive: true,
name: randomString(),
on: ['abuseReport'],
url: WEBHOOK_HOST,
secret: randomString(),
...args,
},
credential ?? admin,
);
return res.body;
}
async function createAbuseReportNotificationRecipient(args?: Partial<entities.AdminAbuseReportNotificationRecipientCreateRequest>, credential?: UserToken): Promise<entities.AdminAbuseReportNotificationRecipientCreateResponse> {
const res = await api(
'admin/abuse-report/notification-recipient/create',
{
isActive: true,
name: randomString(),
method: 'webhook',
...args,
},
credential ?? admin,
);
return res.body;
}
async function createAbuseReport(args?: Partial<entities.UsersReportAbuseRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
const res = await api(
'users/report-abuse',
{
userId: alice.id,
comment: randomString(),
...args,
},
credential ?? admin,
);
return res.body;
}
async function resolveAbuseReport(args?: Partial<entities.AdminResolveAbuseUserReportRequest>, credential?: UserToken): Promise<entities.EmptyResponse> {
const res = await api(
'admin/resolve-abuse-user-report',
{
reportId: admin.id,
...args,
},
credential ?? admin,
);
return res.body;
}
// -------------------------------------------------------------------------------------------
beforeAll(async () => {
queue = await startJobQueue();
admin = await signup({ username: 'admin' });
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
await role(admin, { isAdministrator: true });
}, 1000 * 60 * 2);
afterAll(async () => {
await queue.close();
});
// -------------------------------------------------------------------------------------------
describe('SystemWebhook', () => {
beforeEach(async () => {
const webhooks = await api('admin/system-webhook/list', {}, admin);
for (const webhook of webhooks.body) {
await api('admin/system-webhook/delete', { id: webhook.id }, admin);
}
});
test('通報を受けた -> abuseReportが送出される', async () => {
const webhook = await createSystemWebhook({
on: ['abuseReport'],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
});
console.log(JSON.stringify(webhookBody, null, 2));
expect(webhookBody.hookId).toBe(webhook.id);
expect(webhookBody.type).toBe('abuseReport');
expect(webhookBody.body.targetUserId).toBe(alice.id);
expect(webhookBody.body.reporterId).toBe(bob.id);
expect(webhookBody.body.comment).toBe(abuse.comment);
});
test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが送出される', async () => {
const webhook = await createSystemWebhook({
on: ['abuseReport', 'abuseReportResolved'],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody1 = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
});
console.log(JSON.stringify(webhookBody1, null, 2));
expect(webhookBody1.hookId).toBe(webhook.id);
expect(webhookBody1.type).toBe('abuseReport');
expect(webhookBody1.body.targetUserId).toBe(alice.id);
expect(webhookBody1.body.reporterId).toBe(bob.id);
expect(webhookBody1.body.assigneeId).toBeNull();
expect(webhookBody1.body.resolved).toBe(false);
expect(webhookBody1.body.comment).toBe(abuse.comment);
// 解決
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
forward: false,
}, admin);
});
console.log(JSON.stringify(webhookBody2, null, 2));
expect(webhookBody2.hookId).toBe(webhook.id);
expect(webhookBody2.type).toBe('abuseReportResolved');
expect(webhookBody2.body.targetUserId).toBe(alice.id);
expect(webhookBody2.body.reporterId).toBe(bob.id);
expect(webhookBody2.body.assigneeId).toBe(admin.id);
expect(webhookBody2.body.resolved).toBe(true);
expect(webhookBody2.body.comment).toBe(abuse.comment);
});
test('通報を受けた -> abuseReportが未許可の場合は送出されない', async () => {
const webhook = await createSystemWebhook({
on: [],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
}).catch(e => e.message);
expect(webhookBody).toBe('timeout');
});
test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが送出される', async () => {
const webhook = await createSystemWebhook({
on: ['abuseReportResolved'],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody1 = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
}).catch(e => e.message);
expect(webhookBody1).toBe('timeout');
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
// 解決
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
});
console.log(JSON.stringify(webhookBody2, null, 2));
expect(webhookBody2.hookId).toBe(webhook.id);
expect(webhookBody2.type).toBe('abuseReportResolved');
expect(webhookBody2.body.targetUserId).toBe(alice.id);
expect(webhookBody2.body.reporterId).toBe(bob.id);
expect(webhookBody2.body.assigneeId).toBe(admin.id);
expect(webhookBody2.body.resolved).toBe(true);
expect(webhookBody2.body.comment).toBe(abuse.comment);
});
test('通報を受けた -> abuseReportが送出される -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
const webhook = await createSystemWebhook({
on: ['abuseReport'],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody1 = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
});
console.log(JSON.stringify(webhookBody1, null, 2));
expect(webhookBody1.hookId).toBe(webhook.id);
expect(webhookBody1.type).toBe('abuseReport');
expect(webhookBody1.body.targetUserId).toBe(alice.id);
expect(webhookBody1.body.reporterId).toBe(bob.id);
expect(webhookBody1.body.assigneeId).toBeNull();
expect(webhookBody1.body.resolved).toBe(false);
expect(webhookBody1.body.comment).toBe(abuse.comment);
// 解決
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
forward: false,
}, admin);
}).catch(e => e.message);
expect(webhookBody2).toBe('timeout');
});
test('通報を受けた -> abuseReportが未許可の場合は送出されない -> 解決 -> abuseReportResolvedが未許可の場合は送出されない', async () => {
const webhook = await createSystemWebhook({
on: [],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody1 = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
}).catch(e => e.message);
expect(webhookBody1).toBe('timeout');
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
// 解決
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
}).catch(e => e.message);
expect(webhookBody2).toBe('timeout');
});
test('通報を受けた -> Webhookが無効の場合は送出されない', async () => {
const webhook = await createSystemWebhook({
on: ['abuseReport', 'abuseReportResolved'],
isActive: false,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody1 = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
}).catch(e => e.message);
expect(webhookBody1).toBe('timeout');
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
// 解決
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
}).catch(e => e.message);
expect(webhookBody2).toBe('timeout');
});
test('通報を受けた -> 通知設定が無効の場合は送出されない', async () => {
const webhook = await createSystemWebhook({
on: ['abuseReport', 'abuseReportResolved'],
isActive: true,
});
await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false });
// 通報(bob -> alice)
const abuse = {
userId: alice.id,
comment: randomString(),
};
const webhookBody1 = await captureWebhook(async () => {
await createAbuseReport(abuse, bob);
}).catch(e => e.message);
expect(webhookBody1).toBe('timeout');
const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id;
// 解決
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
forward: false,
}, admin);
}).catch(e => e.message);
expect(webhookBody2).toBe('timeout');
});
});
});