merge upstream 2025-02-03

This commit is contained in:
Hazelnoot 2025-02-03 14:31:26 -05:00
commit a4e86758c1
264 changed files with 15775 additions and 4919 deletions

View file

@ -397,7 +397,7 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false);
assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false);
}, 1000 * 10);
}, 1000 * 15);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);

View file

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { jest } from '@jest/globals';
import { describe, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import {
AbuseReportNotificationRecipientRepository,
MiAbuseReportNotificationRecipient,
MiAbuseUserReport,
MiSystemWebhook,
MiUser,
SystemWebhooksRepository,
@ -112,7 +113,10 @@ describe('AbuseReportNotificationService', () => {
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
},
{
provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
provide: UserEntityService, useFactory: () => ({
pack: (v: any) => Promise.resolve(v),
packMany: (v: any) => Promise.resolve(v),
}),
},
{
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
@ -344,4 +348,46 @@ describe('AbuseReportNotificationService', () => {
expect(recipients).toEqual([recipient3]);
});
});
describe('notifySystemWebhook', () => {
test('非アクティブな通報通知はWebhook送信から除外される', async () => {
const recipient1 = await createRecipient({
method: 'webhook',
systemWebhookId: systemWebhook1.id,
isActive: true,
});
const recipient2 = await createRecipient({
method: 'webhook',
systemWebhookId: systemWebhook2.id,
isActive: false,
});
const reports: MiAbuseUserReport[] = [
{
id: idService.gen(),
targetUserId: alice.id,
targetUser: alice,
reporterId: bob.id,
reporter: bob,
assigneeId: null,
assignee: null,
resolved: false,
forwarded: false,
comment: 'test',
moderationNote: '',
resolvedAs: null,
targetUserHost: null,
reporterHost: null,
},
];
await service.notifySystemWebhook(reports, 'abuseReport');
// 実際に除外されるかはSystemWebhookService側で確認する.
// ここでは非アクティブな通報通知を除外設定できているかを確認する
expect(webhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
expect(webhookService.enqueueSystemWebhook.mock.calls[0][0]).toBe('abuseReport');
expect(webhookService.enqueueSystemWebhook.mock.calls[0][2]).toEqual({ excludes: [systemWebhook2.id] });
});
});
});

View file

@ -0,0 +1,622 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { Response } from 'node-fetch';
import {
CaptchaError,
CaptchaErrorCode,
captchaErrorCodes,
CaptchaSaveResult,
CaptchaService,
} from '@/core/CaptchaService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { MetaService } from '@/core/MetaService.js';
import { MiMeta } from '@/models/Meta.js';
import { LoggerService } from '@/core/LoggerService.js';
describe('CaptchaService', () => {
let app: TestingModule;
let service: CaptchaService;
let httpRequestService: jest.Mocked<HttpRequestService>;
let metaService: jest.Mocked<MetaService>;
beforeAll(async () => {
app = await Test.createTestingModule({
imports: [
GlobalModule,
],
providers: [
CaptchaService,
LoggerService,
{
provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }),
},
{
provide: MetaService, useFactory: () => ({
fetch: jest.fn(),
update: jest.fn(),
}),
},
],
}).compile();
app.enableShutdownHooks();
service = app.get(CaptchaService);
httpRequestService = app.get(HttpRequestService) as jest.Mocked<HttpRequestService>;
metaService = app.get(MetaService) as jest.Mocked<MetaService>;
});
beforeEach(() => {
httpRequestService.send.mockClear();
metaService.update.mockClear();
metaService.fetch.mockClear();
});
afterAll(async () => {
await app.close();
});
function successMock(result: object) {
httpRequestService.send.mockResolvedValue({
ok: true,
status: 200,
json: async () => (result),
} as Response);
}
function failureHttpMock() {
httpRequestService.send.mockResolvedValue({
ok: false,
status: 400,
} as Response);
}
function failureVerificationMock(result: object) {
httpRequestService.send.mockResolvedValue({
ok: true,
status: 200,
json: async () => (result),
} as Response);
}
async function testCaptchaError(code: CaptchaErrorCode, test: () => Promise<void>) {
try {
await test();
expect(false).toBe(true);
} catch (e) {
expect(e instanceof CaptchaError).toBe(true);
const _e = e as CaptchaError;
expect(_e.code).toBe(code);
}
}
describe('verifyRecaptcha', () => {
test('success', async () => {
successMock({ success: true });
await service.verifyRecaptcha('secret', 'response');
});
test('noResponseProvided', async () => {
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyRecaptcha('secret', null));
});
test('requestFailed', async () => {
failureHttpMock();
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyRecaptcha('secret', 'response'));
});
test('verificationFailed', async () => {
failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyRecaptcha('secret', 'response'));
});
});
describe('verifyHcaptcha', () => {
test('success', async () => {
successMock({ success: true });
await service.verifyHcaptcha('secret', 'response');
});
test('noResponseProvided', async () => {
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyHcaptcha('secret', null));
});
test('requestFailed', async () => {
failureHttpMock();
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyHcaptcha('secret', 'response'));
});
test('verificationFailed', async () => {
failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyHcaptcha('secret', 'response'));
});
});
describe('verifyMcaptcha', () => {
const host = 'https://localhost';
test('success', async () => {
successMock({ valid: true });
await service.verifyMcaptcha('secret', 'sitekey', host, 'response');
});
test('noResponseProvided', async () => {
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyMcaptcha('secret', 'sitekey', host, null));
});
test('requestFailed', async () => {
failureHttpMock();
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response'));
});
test('verificationFailed', async () => {
failureVerificationMock({ valid: false });
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response'));
});
});
describe('verifyTurnstile', () => {
test('success', async () => {
successMock({ success: true });
await service.verifyTurnstile('secret', 'response');
});
test('noResponseProvided', async () => {
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTurnstile('secret', null));
});
test('requestFailed', async () => {
failureHttpMock();
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyTurnstile('secret', 'response'));
});
test('verificationFailed', async () => {
failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTurnstile('secret', 'response'));
});
});
describe('verifyTestcaptcha', () => {
test('success', async () => {
await service.verifyTestcaptcha('testcaptcha-passed');
});
test('noResponseProvided', async () => {
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTestcaptcha(null));
});
test('verificationFailed', async () => {
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTestcaptcha('testcaptcha-failed'));
});
});
describe('get', () => {
function setupMeta(meta: Partial<MiMeta>) {
metaService.fetch.mockResolvedValue(meta as MiMeta);
}
test('values', async () => {
setupMeta({
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
hcaptchaSiteKey: 'hcaptcha-sitekey',
hcaptchaSecretKey: 'hcaptcha-secret',
mcaptchaSitekey: 'mcaptcha-sitekey',
mcaptchaSecretKey: 'mcaptcha-secret',
mcaptchaInstanceUrl: 'https://localhost',
recaptchaSiteKey: 'recaptcha-sitekey',
recaptchaSecretKey: 'recaptcha-secret',
turnstileSiteKey: 'turnstile-sitekey',
turnstileSecretKey: 'turnstile-secret',
});
const result = await service.get();
expect(result.provider).toBe('none');
expect(result.hcaptcha.siteKey).toBe('hcaptcha-sitekey');
expect(result.hcaptcha.secretKey).toBe('hcaptcha-secret');
expect(result.mcaptcha.siteKey).toBe('mcaptcha-sitekey');
expect(result.mcaptcha.secretKey).toBe('mcaptcha-secret');
expect(result.mcaptcha.instanceUrl).toBe('https://localhost');
expect(result.recaptcha.siteKey).toBe('recaptcha-sitekey');
expect(result.recaptcha.secretKey).toBe('recaptcha-secret');
expect(result.turnstile.siteKey).toBe('turnstile-sitekey');
expect(result.turnstile.secretKey).toBe('turnstile-secret');
});
describe('provider', () => {
test('none', async () => {
setupMeta({
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
});
const result = await service.get();
expect(result.provider).toBe('none');
});
test('hcaptcha', async () => {
setupMeta({
enableHcaptcha: true,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
});
const result = await service.get();
expect(result.provider).toBe('hcaptcha');
});
test('mcaptcha', async () => {
setupMeta({
enableHcaptcha: false,
enableMcaptcha: true,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
});
const result = await service.get();
expect(result.provider).toBe('mcaptcha');
});
test('recaptcha', async () => {
setupMeta({
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: true,
enableTurnstile: false,
enableTestcaptcha: false,
});
const result = await service.get();
expect(result.provider).toBe('recaptcha');
});
test('turnstile', async () => {
setupMeta({
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: true,
enableTestcaptcha: false,
});
const result = await service.get();
expect(result.provider).toBe('turnstile');
});
test('testcaptcha', async () => {
setupMeta({
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: true,
});
const result = await service.get();
expect(result.provider).toBe('testcaptcha');
});
});
});
describe('save', () => {
const host = 'https://localhost';
describe('[success] 検証に成功した時だけ保存できる+他のプロバイダの設定値を誤って更新しない', () => {
beforeEach(() => {
successMock({ success: true, valid: true });
});
async function assertSuccess(promise: Promise<CaptchaSaveResult>, expectMeta: Partial<MiMeta>) {
await expect(promise)
.resolves
.toStrictEqual({ success: true });
const partialParams = metaService.update.mock.calls[0][0];
expect(partialParams).toStrictEqual(expectMeta);
}
test('none', async () => {
await assertSuccess(
service.save('none'),
{
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
},
);
});
test('hcaptcha', async () => {
await assertSuccess(
service.save('hcaptcha', {
sitekey: 'hcaptcha-sitekey',
secret: 'hcaptcha-secret',
captchaResult: 'hcaptcha-passed',
}),
{
enableHcaptcha: true,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
hcaptchaSiteKey: 'hcaptcha-sitekey',
hcaptchaSecretKey: 'hcaptcha-secret',
},
);
});
test('mcaptcha', async () => {
await assertSuccess(
service.save('mcaptcha', {
sitekey: 'mcaptcha-sitekey',
secret: 'mcaptcha-secret',
instanceUrl: host,
captchaResult: 'mcaptcha-passed',
}),
{
enableHcaptcha: false,
enableMcaptcha: true,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: false,
mcaptchaSitekey: 'mcaptcha-sitekey',
mcaptchaSecretKey: 'mcaptcha-secret',
mcaptchaInstanceUrl: host,
},
);
});
test('recaptcha', async () => {
await assertSuccess(
service.save('recaptcha', {
sitekey: 'recaptcha-sitekey',
secret: 'recaptcha-secret',
captchaResult: 'recaptcha-passed',
}),
{
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: true,
enableTurnstile: false,
enableTestcaptcha: false,
recaptchaSiteKey: 'recaptcha-sitekey',
recaptchaSecretKey: 'recaptcha-secret',
},
);
});
test('turnstile', async () => {
await assertSuccess(
service.save('turnstile', {
sitekey: 'turnstile-sitekey',
secret: 'turnstile-secret',
captchaResult: 'turnstile-passed',
}),
{
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: true,
enableTestcaptcha: false,
turnstileSiteKey: 'turnstile-sitekey',
turnstileSecretKey: 'turnstile-secret',
},
);
});
test('testcaptcha', async () => {
await assertSuccess(
service.save('testcaptcha', {
sitekey: 'testcaptcha-sitekey',
secret: 'testcaptcha-secret',
captchaResult: 'testcaptcha-passed',
}),
{
enableHcaptcha: false,
enableMcaptcha: false,
enableRecaptcha: false,
enableTurnstile: false,
enableTestcaptcha: true,
},
);
});
});
describe('[failure] 検証に失敗した場合は保存できない+設定値の更新そのものが発生しない', () => {
async function assertFailure(code: CaptchaErrorCode, promise: Promise<CaptchaSaveResult>) {
const res = await promise;
expect(res.success).toBe(false);
if (!res.success) {
expect(res.error.code).toBe(code);
}
expect(metaService.update).not.toBeCalled();
}
describe('invalidParameters', () => {
test('hcaptcha', async () => {
await assertFailure(
captchaErrorCodes.invalidParameters,
service.save('hcaptcha', {
sitekey: 'hcaptcha-sitekey',
secret: 'hcaptcha-secret',
captchaResult: null,
}),
);
});
test('mcaptcha', async () => {
await assertFailure(
captchaErrorCodes.invalidParameters,
service.save('mcaptcha', {
sitekey: 'mcaptcha-sitekey',
secret: 'mcaptcha-secret',
instanceUrl: host,
captchaResult: null,
}),
);
});
test('recaptcha', async () => {
await assertFailure(
captchaErrorCodes.invalidParameters,
service.save('recaptcha', {
sitekey: 'recaptcha-sitekey',
secret: 'recaptcha-secret',
captchaResult: null,
}),
);
});
test('turnstile', async () => {
await assertFailure(
captchaErrorCodes.invalidParameters,
service.save('turnstile', {
sitekey: 'turnstile-sitekey',
secret: 'turnstile-secret',
captchaResult: null,
}),
);
});
test('testcaptcha', async () => {
await assertFailure(
captchaErrorCodes.invalidParameters,
service.save('testcaptcha', {
captchaResult: null,
}),
);
});
});
describe('requestFailed', () => {
beforeEach(() => {
failureHttpMock();
});
test('hcaptcha', async () => {
await assertFailure(
captchaErrorCodes.requestFailed,
service.save('hcaptcha', {
sitekey: 'hcaptcha-sitekey',
secret: 'hcaptcha-secret',
captchaResult: 'hcaptcha-passed',
}),
);
});
test('mcaptcha', async () => {
await assertFailure(
captchaErrorCodes.requestFailed,
service.save('mcaptcha', {
sitekey: 'mcaptcha-sitekey',
secret: 'mcaptcha-secret',
instanceUrl: host,
captchaResult: 'mcaptcha-passed',
}),
);
});
test('recaptcha', async () => {
await assertFailure(
captchaErrorCodes.requestFailed,
service.save('recaptcha', {
sitekey: 'recaptcha-sitekey',
secret: 'recaptcha-secret',
captchaResult: 'recaptcha-passed',
}),
);
});
test('turnstile', async () => {
await assertFailure(
captchaErrorCodes.requestFailed,
service.save('turnstile', {
sitekey: 'turnstile-sitekey',
secret: 'turnstile-secret',
captchaResult: 'turnstile-passed',
}),
);
});
// testchapchaはrequestFailedがない
});
describe('verificationFailed', () => {
beforeEach(() => {
failureVerificationMock({ success: false, valid: false, 'error-codes': ['code01', 'code02'] });
});
test('hcaptcha', async () => {
await assertFailure(
captchaErrorCodes.verificationFailed,
service.save('hcaptcha', {
sitekey: 'hcaptcha-sitekey',
secret: 'hcaptcha-secret',
captchaResult: 'hccaptcha-passed',
}),
);
});
test('mcaptcha', async () => {
await assertFailure(
captchaErrorCodes.verificationFailed,
service.save('mcaptcha', {
sitekey: 'mcaptcha-sitekey',
secret: 'mcaptcha-secret',
instanceUrl: host,
captchaResult: 'mcaptcha-passed',
}),
);
});
test('recaptcha', async () => {
await assertFailure(
captchaErrorCodes.verificationFailed,
service.save('recaptcha', {
sitekey: 'recaptcha-sitekey',
secret: 'recaptcha-secret',
captchaResult: 'recaptcha-passed',
}),
);
});
test('turnstile', async () => {
await assertFailure(
captchaErrorCodes.verificationFailed,
service.save('turnstile', {
sitekey: 'turnstile-sitekey',
secret: 'turnstile-secret',
captchaResult: 'turnstile-passed',
}),
);
});
test('testcaptcha', async () => {
await assertFailure(
captchaErrorCodes.verificationFailed,
service.save('testcaptcha', {
captchaResult: 'testcaptcha-failed',
}),
);
});
});
});
});
});

View file

@ -0,0 +1,817 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { afterEach, beforeAll, describe, test } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import { GlobalModule } from '@/GlobalModule.js';
import { EmojisRepository } from '@/models/_.js';
import { MiEmoji } from '@/models/Emoji.js';
describe('CustomEmojiService', () => {
let app: TestingModule;
let service: CustomEmojiService;
let emojisRepository: EmojisRepository;
let idService: IdService;
beforeAll(async () => {
app = await Test
.createTestingModule({
imports: [
GlobalModule,
],
providers: [
CustomEmojiService,
UtilityService,
IdService,
EmojiEntityService,
ModerationLogService,
GlobalEventService,
],
})
.compile();
app.enableShutdownHooks();
service = app.get<CustomEmojiService>(CustomEmojiService);
emojisRepository = app.get<EmojisRepository>(DI.emojisRepository);
idService = app.get<IdService>(IdService);
});
describe('fetchEmojis', () => {
async function insert(data: Partial<MiEmoji>[]) {
for (const d of data) {
const id = idService.gen();
await emojisRepository.insert({
id: id,
updatedAt: new Date(),
...d,
});
}
}
function call(params: Parameters<CustomEmojiService['fetchEmojis']>['0']) {
return service.fetchEmojis(
params,
{
// テスト向けに
sortKeys: ['+id'],
},
);
}
function defaultData(suffix: string, override?: Partial<MiEmoji>): Partial<MiEmoji> {
return {
name: `emoji${suffix}`,
host: null,
category: 'default',
originalUrl: `https://example.com/emoji${suffix}.png`,
publicUrl: `https://example.com/emoji${suffix}.png`,
type: 'image/png',
aliases: [`emoji${suffix}`],
license: 'CC0',
isSensitive: false,
localOnly: false,
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
...override,
};
}
afterEach(async () => {
await emojisRepository.delete({});
});
describe('単独', () => {
test('updatedAtFrom', async () => {
await insert([
defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }),
defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }),
defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }),
]);
const actual = await call({
query: {
updatedAtFrom: '2021-01-02T00:00:00.000Z',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji002');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('updatedAtTo', async () => {
await insert([
defaultData('001', { updatedAt: new Date('2021-01-01T00:00:00.000Z') }),
defaultData('002', { updatedAt: new Date('2021-01-02T00:00:00.000Z') }),
defaultData('003', { updatedAt: new Date('2021-01-03T00:00:00.000Z') }),
]);
const actual = await call({
query: {
updatedAtTo: '2021-01-02T00:00:00.000Z',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji002');
});
describe('name', () => {
test('single', async () => {
await insert([
defaultData('001'),
defaultData('002'),
]);
const actual = await call({
query: {
name: 'emoji001',
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji001');
});
test('multi', async () => {
await insert([
defaultData('001'),
defaultData('002'),
]);
const actual = await call({
query: {
name: 'emoji001 emoji002',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji002');
});
test('keyword', async () => {
await insert([
defaultData('001'),
defaultData('002'),
defaultData('003', { name: 'em003' }),
]);
const actual = await call({
query: {
name: 'oji',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji002');
});
test('escape', async () => {
await insert([
defaultData('001'),
]);
const actual = await call({
query: {
name: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('host', () => {
test('single', async () => {
await insert([
defaultData('001', { host: 'example.com' }),
defaultData('002', { host: 'example.com' }),
defaultData('003', { host: '1.example.com' }),
defaultData('004', { host: '2.example.com' }),
]);
const actual = await call({
query: {
host: 'example.com',
hostType: 'remote',
},
});
expect(actual.allCount).toBe(4);
});
test('multi', async () => {
await insert([
defaultData('001', { host: 'example.com' }),
defaultData('002', { host: 'example.com' }),
defaultData('003', { host: '1.example.com' }),
defaultData('004', { host: '2.example.com' }),
]);
const actual = await call({
query: {
host: '1.example.com 2.example.com',
hostType: 'remote',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji003');
expect(actual.emojis[1].name).toBe('emoji004');
});
test('keyword', async () => {
await insert([
defaultData('001', { host: 'example.com' }),
defaultData('002', { host: 'example.com' }),
defaultData('003', { host: '1.example.com' }),
defaultData('004', { host: '2.example.com' }),
]);
const actual = await call({
query: {
host: 'example',
hostType: 'remote',
},
});
expect(actual.allCount).toBe(4);
});
test('escape', async () => {
await insert([
defaultData('001', { host: 'example.com' }),
]);
const actual = await call({
query: {
host: '%',
hostType: 'remote',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('uri', () => {
test('single', async () => {
await insert([
defaultData('001', { uri: 'uri001' }),
defaultData('002', { uri: 'uri002' }),
defaultData('003', { uri: 'uri003' }),
]);
const actual = await call({
query: {
uri: 'uri002',
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { uri: 'uri001' }),
defaultData('002', { uri: 'uri002' }),
defaultData('003', { uri: 'uri003' }),
]);
const actual = await call({
query: {
uri: 'uri001 uri003',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('keyword', async () => {
await insert([
defaultData('001', { uri: 'uri001' }),
defaultData('002', { uri: 'uri002' }),
defaultData('003', { uri: 'uri003' }),
]);
const actual = await call({
query: {
uri: 'ri',
},
});
expect(actual.allCount).toBe(3);
});
test('escape', async () => {
await insert([
defaultData('001', { uri: 'uri001' }),
]);
const actual = await call({
query: {
uri: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('publicUrl', () => {
test('single', async () => {
await insert([
defaultData('001', { publicUrl: 'publicUrl001' }),
defaultData('002', { publicUrl: 'publicUrl002' }),
defaultData('003', { publicUrl: 'publicUrl003' }),
]);
const actual = await call({
query: {
publicUrl: 'publicUrl002',
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { publicUrl: 'publicUrl001' }),
defaultData('002', { publicUrl: 'publicUrl002' }),
defaultData('003', { publicUrl: 'publicUrl003' }),
]);
const actual = await call({
query: {
publicUrl: 'publicUrl001 publicUrl003',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('keyword', async () => {
await insert([
defaultData('001', { publicUrl: 'publicUrl001' }),
defaultData('002', { publicUrl: 'publicUrl002' }),
defaultData('003', { publicUrl: 'publicUrl003' }),
]);
const actual = await call({
query: {
publicUrl: 'Url',
},
});
expect(actual.allCount).toBe(3);
});
test('escape', async () => {
await insert([
defaultData('001', { publicUrl: 'publicUrl001' }),
]);
const actual = await call({
query: {
publicUrl: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('type', () => {
test('single', async () => {
await insert([
defaultData('001', { type: 'type001' }),
defaultData('002', { type: 'type002' }),
defaultData('003', { type: 'type003' }),
]);
const actual = await call({
query: {
type: 'type002',
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { type: 'type001' }),
defaultData('002', { type: 'type002' }),
defaultData('003', { type: 'type003' }),
]);
const actual = await call({
query: {
type: 'type001 type003',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('keyword', async () => {
await insert([
defaultData('001', { type: 'type001' }),
defaultData('002', { type: 'type002' }),
defaultData('003', { type: 'type003' }),
]);
const actual = await call({
query: {
type: 'pe',
},
});
expect(actual.allCount).toBe(3);
});
test('escape', async () => {
await insert([
defaultData('001', { type: 'type001' }),
]);
const actual = await call({
query: {
type: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('aliases', () => {
test('single', async () => {
await insert([
defaultData('001', { aliases: ['alias001', 'alias002'] }),
defaultData('002', { aliases: ['alias002'] }),
defaultData('003', { aliases: ['alias003'] }),
]);
const actual = await call({
query: {
aliases: 'alias002',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { aliases: ['alias001', 'alias002'] }),
defaultData('002', { aliases: ['alias002', 'alias004'] }),
defaultData('003', { aliases: ['alias003'] }),
defaultData('004', { aliases: ['alias004'] }),
]);
const actual = await call({
query: {
aliases: 'alias001 alias004',
},
});
expect(actual.allCount).toBe(3);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji002');
expect(actual.emojis[2].name).toBe('emoji004');
});
test('keyword', async () => {
await insert([
defaultData('001', { aliases: ['alias001', 'alias002'] }),
defaultData('002', { aliases: ['alias002', 'alias004'] }),
defaultData('003', { aliases: ['alias003'] }),
defaultData('004', { aliases: ['alias004'] }),
]);
const actual = await call({
query: {
aliases: 'ias',
},
});
expect(actual.allCount).toBe(4);
});
test('escape', async () => {
await insert([
defaultData('001', { aliases: ['alias001', 'alias002'] }),
]);
const actual = await call({
query: {
aliases: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('category', () => {
test('single', async () => {
await insert([
defaultData('001', { category: 'category001' }),
defaultData('002', { category: 'category002' }),
defaultData('003', { category: 'category003' }),
]);
const actual = await call({
query: {
category: 'category002',
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { category: 'category001' }),
defaultData('002', { category: 'category002' }),
defaultData('003', { category: 'category003' }),
]);
const actual = await call({
query: {
category: 'category001 category003',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('keyword', async () => {
await insert([
defaultData('001', { category: 'category001' }),
defaultData('002', { category: 'category002' }),
defaultData('003', { category: 'category003' }),
]);
const actual = await call({
query: {
category: 'egory',
},
});
expect(actual.allCount).toBe(3);
});
test('escape', async () => {
await insert([
defaultData('001', { category: 'category001' }),
]);
const actual = await call({
query: {
category: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('license', () => {
test('single', async () => {
await insert([
defaultData('001', { license: 'license001' }),
defaultData('002', { license: 'license002' }),
defaultData('003', { license: 'license003' }),
]);
const actual = await call({
query: {
license: 'license002',
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { license: 'license001' }),
defaultData('002', { license: 'license002' }),
defaultData('003', { license: 'license003' }),
]);
const actual = await call({
query: {
license: 'license001 license003',
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('keyword', async () => {
await insert([
defaultData('001', { license: 'license001' }),
defaultData('002', { license: 'license002' }),
defaultData('003', { license: 'license003' }),
]);
const actual = await call({
query: {
license: 'cense',
},
});
expect(actual.allCount).toBe(3);
});
test('escape', async () => {
await insert([
defaultData('001', { license: 'license001' }),
]);
const actual = await call({
query: {
license: '%',
},
});
expect(actual.allCount).toBe(0);
});
});
describe('isSensitive', () => {
test('true', async () => {
await insert([
defaultData('001', { isSensitive: true }),
defaultData('002', { isSensitive: false }),
defaultData('003', { isSensitive: true }),
]);
const actual = await call({
query: {
isSensitive: true,
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('false', async () => {
await insert([
defaultData('001', { isSensitive: true }),
defaultData('002', { isSensitive: false }),
defaultData('003', { isSensitive: true }),
]);
const actual = await call({
query: {
isSensitive: false,
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('null', async () => {
await insert([
defaultData('001', { isSensitive: true }),
defaultData('002', { isSensitive: false }),
defaultData('003', { isSensitive: true }),
]);
const actual = await call({
query: {},
});
expect(actual.allCount).toBe(3);
});
});
describe('localOnly', () => {
test('true', async () => {
await insert([
defaultData('001', { localOnly: true }),
defaultData('002', { localOnly: false }),
defaultData('003', { localOnly: true }),
]);
const actual = await call({
query: {
localOnly: true,
},
});
expect(actual.allCount).toBe(2);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji003');
});
test('false', async () => {
await insert([
defaultData('001', { localOnly: true }),
defaultData('002', { localOnly: false }),
defaultData('003', { localOnly: true }),
]);
const actual = await call({
query: {
localOnly: false,
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('null', async () => {
await insert([
defaultData('001', { localOnly: true }),
defaultData('002', { localOnly: false }),
defaultData('003', { localOnly: true }),
]);
const actual = await call({
query: {},
});
expect(actual.allCount).toBe(3);
});
});
describe('roleId', () => {
test('single', async () => {
await insert([
defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }),
defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002'] }),
defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }),
]);
const actual = await call({
query: {
roleIds: ['role002'],
},
});
expect(actual.allCount).toBe(1);
expect(actual.emojis[0].name).toBe('emoji002');
});
test('multi', async () => {
await insert([
defaultData('001', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role001'] }),
defaultData('002', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role002', 'role003'] }),
defaultData('003', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role003'] }),
defaultData('004', { roleIdsThatCanBeUsedThisEmojiAsReaction: ['role004'] }),
]);
const actual = await call({
query: {
roleIds: ['role001', 'role003'],
},
});
expect(actual.allCount).toBe(3);
expect(actual.emojis[0].name).toBe('emoji001');
expect(actual.emojis[1].name).toBe('emoji002');
expect(actual.emojis[2].name).toBe('emoji003');
});
});
});
});
});

View file

@ -108,6 +108,24 @@ describe('MfmService', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a></a> d</p>'), 'a d');
});
test('ruby', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスキー] b');
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスキー]$[ruby Misskey ミスキー] b');
});
test('ruby with spaces', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Miss key<rp>(</rp><rt>ミスキー</rt><rp>)</rp> b</ruby> c</p>'), 'a Miss key(ミスキー) b c');
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp> b</ruby> c</p>'), 'a Misskey(ミス キー) b c');
assert.deepStrictEqual(
mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'),
'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b'
);
});
test('ruby with other inline tags', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby><strong>Misskey</strong><rp>(</rp><rt>ミスキー</rt><rp>)</rp> b</ruby> c</p>'), 'a **Misskey**(ミスキー) b c');
});
test('mention', () => {
assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
});

View file

@ -314,9 +314,10 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReport'],
});
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1);
expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook);
});
test('非アクティブなWebhookはキューに追加されない', async () => {
@ -324,7 +325,7 @@ describe('SystemWebhookService', () => {
isActive: false,
on: ['abuseReport'],
});
await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
@ -338,11 +339,49 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReportResolved'],
});
await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
test('混在した時、有効かつ許可されたイベント種別のみ', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: true,
on: ['abuseReportResolved'],
});
const webhook3 = await createWebhook({
isActive: false,
on: ['abuseReport'],
});
const webhook4 = await createWebhook({
isActive: false,
on: ['abuseReportResolved'],
});
await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1);
expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1);
});
test('除外指定した場合は送信されない', async () => {
const webhook1 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
const webhook2 = await createWebhook({
isActive: true,
on: ['abuseReport'],
});
await service.enqueueSystemWebhook('abuseReport', { foo: 'bar' } as any, { excludes: [webhook2.id] });
expect(queueService.systemWebhookDeliver).toHaveBeenCalledTimes(1);
expect(queueService.systemWebhookDeliver.mock.calls[0][0] as MiSystemWebhook).toEqual(webhook1);
});
});
describe('fetchActiveSystemWebhooks', () => {

View file

@ -1,4 +1,3 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
@ -71,7 +70,7 @@ describe('UserWebhookService', () => {
LoggerService,
GlobalEventService,
{
provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
provide: QueueService, useFactory: () => ({ userWebhookDeliver: jest.fn() }),
},
],
})
@ -242,4 +241,92 @@ describe('UserWebhookService', () => {
});
});
});
describe('アプリを毎回作り直す必要があるグループ', () => {
beforeEach(async () => {
await beforeAllImpl();
await beforeEachImpl();
});
afterEach(async () => {
await afterEachImpl();
await afterAllImpl();
});
describe('enqueueUserWebhook', () => {
test('キューに追加成功', async () => {
const webhook = await createWebhook({
active: true,
on: ['note'],
});
await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any);
expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1);
expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook);
});
test('非アクティブなWebhookはキューに追加されない', async () => {
const webhook = await createWebhook({
active: false,
on: ['note'],
});
await service.enqueueUserWebhook(webhook.userId, 'note', { foo: 'bar' } as any);
expect(queueService.userWebhookDeliver).not.toHaveBeenCalled();
});
test('未許可のイベント種別が渡された場合はWebhookはキューに追加されない', async () => {
const webhook1 = await createWebhook({
active: true,
on: [],
});
const webhook2 = await createWebhook({
active: true,
on: ['note'],
});
await service.enqueueUserWebhook(webhook1.userId, 'renote', { foo: 'bar' } as any);
await service.enqueueUserWebhook(webhook2.userId, 'renote', { foo: 'bar' } as any);
expect(queueService.userWebhookDeliver).not.toHaveBeenCalled();
});
test('ユーザIDが異なるWebhookはキューに追加されない', async () => {
const webhook = await createWebhook({
active: true,
on: ['note'],
});
await service.enqueueUserWebhook(idService.gen(), 'note', { foo: 'bar' } as any);
expect(queueService.userWebhookDeliver).not.toHaveBeenCalled();
});
test('混在した時、有効かつ許可されたイベント種別のみ', async () => {
const userId = root.id;
const webhook1 = await createWebhook({
userId,
active: true,
on: ['note'],
});
const webhook2 = await createWebhook({
userId,
active: true,
on: ['renote'],
});
const webhook3 = await createWebhook({
userId,
active: false,
on: ['note'],
});
const webhook4 = await createWebhook({
userId,
active: false,
on: ['renote'],
});
await service.enqueueUserWebhook(userId, 'note', { foo: 'bar' } as any);
expect(queueService.userWebhookDeliver).toHaveBeenCalledTimes(1);
expect(queueService.userWebhookDeliver.mock.calls[0][0] as MiWebhook).toEqual(webhook1);
});
});
});
});

View file

@ -18,6 +18,7 @@ import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
import { EmailService } from '@/core/EmailService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { SystemWebhookEventType } from '@/models/SystemWebhook.js';
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
@ -334,9 +335,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
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);
// typeとactiveによる絞り込みが機能しているかはSystemWebhookServiceのテストで確認する.
// ここでは呼び出されているか、typeが正しいかのみを確認する
expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsWarning');
});
});
@ -372,8 +374,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
mockModeratorRole([user1]);
await service.notifyChangeToInvitationOnly();
// typeとactiveによる絞り込みが機能しているかはSystemWebhookServiceのテストで確認する.
// ここでは呼び出されているか、typeが正しいかのみを確認する
expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0] as SystemWebhookEventType).toEqual('inactiveModeratorsInvitationOnlyChanged');
});
});
});