summaryrefslogtreecommitdiff
path: root/packages/backend/test
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-03-25 16:14:53 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-03-25 16:14:53 -0400
commitd8908ef2d8fa84d8e0fc1d30ab90a600a3d88054 (patch)
tree0c8d3e0385ce7021c7187ef8b608f1abd87496e5 /packages/backend/test
parentmerge: enhance: Update de-DE.yml (!949) (diff)
parentenhance(frontend): 設定の移行を手動でトリガーできるように (diff)
downloadsharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.gz
sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.bz2
sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.zip
merge upstream
Diffstat (limited to 'packages/backend/test')
-rw-r--r--packages/backend/test/e2e/clips.ts36
-rw-r--r--packages/backend/test/e2e/fetch-resource.ts40
-rw-r--r--packages/backend/test/e2e/mute.ts22
-rw-r--r--packages/backend/test/e2e/oauth.ts56
-rw-r--r--packages/backend/test/e2e/thread-mute.ts42
-rw-r--r--packages/backend/test/e2e/timelines.ts2
-rw-r--r--packages/backend/test/e2e/users.ts6
-rw-r--r--packages/backend/test/misc/mock-resolver.ts9
-rw-r--r--packages/backend/test/unit/AbuseReportNotificationService.ts6
-rw-r--r--packages/backend/test/unit/FlashService.ts6
-rw-r--r--packages/backend/test/unit/MfmService.ts4
-rw-r--r--packages/backend/test/unit/RelayService.ts24
-rw-r--r--packages/backend/test/unit/RoleService.ts22
-rw-r--r--packages/backend/test/unit/SystemWebhookService.ts2
-rw-r--r--packages/backend/test/unit/UserSearchService.ts2
-rw-r--r--packages/backend/test/unit/UserWebhookService.ts2
-rw-r--r--packages/backend/test/unit/WebhookTestService.ts10
-rw-r--r--packages/backend/test/unit/ap-request.ts110
-rw-r--r--packages/backend/test/unit/entities/UserEntityService.ts2
-rw-r--r--packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts4
-rw-r--r--packages/backend/test/utils.ts6
21 files changed, 269 insertions, 144 deletions
diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts
index a130c3698d..7ae1ee4523 100644
--- a/packages/backend/test/e2e/clips.ts
+++ b/packages/backend/test/e2e/clips.ts
@@ -182,7 +182,6 @@ describe('クリップ', () => {
{ label: 'nameがnull', parameters: { name: null } },
{ label: 'nameが最大長+1', parameters: { name: 'x'.repeat(101) } },
{ label: 'isPublicがboolじゃない', parameters: { isPublic: 'true' } },
- { label: 'descriptionがゼロ長', parameters: { description: '' } },
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
];
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
@@ -199,6 +198,23 @@ describe('クリップ', () => {
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
}));
+ test('の作成はdescriptionが空文字ならnullになる', async () => {
+ const clip = await successfulApiCall({
+ endpoint: 'clips/create',
+ parameters: {
+ ...defaultCreate(),
+ description: '',
+ },
+ user: alice,
+ });
+
+ assert.deepStrictEqual(clip, {
+ ...clip,
+ ...defaultCreate(),
+ description: null,
+ });
+ });
+
test('の更新ができる', async () => {
const res = await update({
clipId: (await create()).id,
@@ -249,6 +265,24 @@ describe('クリップ', () => {
...assertion,
}));
+ test('の更新はdescriptionが空文字ならnullになる', async () => {
+ const clip = await successfulApiCall({
+ endpoint: 'clips/update',
+ parameters: {
+ clipId: (await create()).id,
+ name: 'updated',
+ description: '',
+ },
+ user: alice,
+ });
+
+ assert.deepStrictEqual(clip, {
+ ...clip,
+ name: 'updated',
+ description: null,
+ });
+ });
+
test('の削除ができる', async () => {
await deleteClip({
clipId: (await create()).id,
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
index 0322ac5546..740295bda8 100644
--- a/packages/backend/test/e2e/fetch-resource.ts
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -6,17 +6,17 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
-import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
+import { channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
import type { SimpleGetResponse } from '../utils.js';
import type * as misskey from 'misskey-js';
-// Request Accept
+// Request Accept in lowercase
const ONLY_AP = 'application/activity+json';
const PREFER_AP = 'application/activity+json, */*';
const PREFER_HTML = 'text/html, */*';
const UNSPECIFIED = '*/*';
-// Response Content-Type
+// Response Content-Type in lowercase
const AP = 'application/activity+json; charset=utf-8';
const HTML = 'text/html; charset=utf-8';
const JSON_UTF8 = 'application/json; charset=utf-8';
@@ -44,7 +44,8 @@ describe('Webリソース', () => {
const { path, accept, cookie, type } = param;
const res = await simpleGet(path, accept, cookie);
assert.strictEqual(res.status, 200);
- assert.strictEqual(res.type, type ?? HTML);
+ // Header values are case-insensitive
+ assert.strictEqual(res.type?.toLowerCase(), (type ?? HTML).toLowerCase());
return res;
};
@@ -95,8 +96,7 @@ describe('Webリソース', () => {
describe.each([
{ path: '/', type: HTML },
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
- // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
- { path: '/api-doc', type: 'text/html; charset=UTF-8' },
+ { path: '/api-doc', type: HTML },
{ path: '/api.json', type: JSON_UTF8 },
{ path: '/api-console', type: HTML },
{ path: '/_info_card_', type: HTML },
@@ -156,17 +156,17 @@ describe('Webリソース', () => {
describe(' has entry such ', () => {
beforeEach(() => {
- post(alice, { text: "**a**" });
+ post(alice, { text: '**a**' });
});
test('MFMを含まない。', async () => {
- const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text());
+ const content = await simpleGet(path(alice.username), '*/*', undefined, res => res.text());
const _body: unknown = content.body;
// JSONフィードのときは改めて文字列化する
- const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string;
+ const body: string = typeof (_body) === 'object' ? JSON.stringify(_body) : _body as string;
- if (body.includes("**a**")) {
- throw new Error("MFM shouldn't be included");
+ if (body.includes('**a**')) {
+ throw new Error('MFM shouldn\'t be included');
}
});
});
@@ -180,24 +180,6 @@ describe('Webリソース', () => {
}));
});
- describe.each([{ path: '/queue' }])('$path', ({ path }) => {
- test('はログインしないとGETできない。', async () => await notOk({
- path,
- status: 401,
- }));
-
- test('はadminでなければGETできない。', async () => await notOk({
- path,
- cookie: cookie(bob),
- status: 403,
- }));
-
- test('はadminならGETできる。', async () => await ok({
- path,
- cookie: cookie(alice),
- }));
- });
-
describe.each([{ path: '/streaming' }])('$path', ({ path }) => {
test('はGETできない。', async () => await notOk({
path,
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
index f37da288b7..b464c24287 100644
--- a/packages/backend/test/e2e/mute.ts
+++ b/packages/backend/test/e2e/mute.ts
@@ -51,30 +51,8 @@ describe('Mute', () => {
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
- test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- await post(carol, { text: '@alice hi' });
-
- const res = await api('i', {}, alice);
-
- assert.strictEqual(res.status, 200);
- assert.strictEqual(res.body.hasUnreadMentions, false);
- });
-
- test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
-
- assert.strictEqual(fired, false);
- });
-
test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts
index ef7a6a579d..f639f90ea6 100644
--- a/packages/backend/test/e2e/oauth.ts
+++ b/packages/backend/test/e2e/oauth.ts
@@ -72,11 +72,12 @@ const clientConfig: ModuleOptions<'client_id'> = {
},
};
-function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } {
+function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined, clientLogo: string | undefined } {
const fragment = JSDOM.fragment(html);
return {
transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content,
clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content,
+ clientLogo: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-logo"]')?.content,
};
}
@@ -915,6 +916,59 @@ describe('OAuth', () => {
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
});
+ test('With Logo', async () => {
+ sender = (reply): void => {
+ reply.header('Link', '</redirect>; rel="redirect_uri"');
+ reply.send(`
+ <!DOCTYPE html>
+ <div class="h-app">
+ <a href="/" class="u-url p-name">Misklient</a>
+ <img src="/logo.png" class="u-logo" />
+ </div>
+ `);
+ reply.send();
+ };
+
+ const client = new AuthorizationCode(clientConfig);
+
+ const response = await fetch(client.authorizeURL({
+ redirect_uri,
+ scope: 'write:notes',
+ state: 'state',
+ code_challenge: 'code',
+ code_challenge_method: 'S256',
+ } as AuthorizationParamsExtended));
+ assert.strictEqual(response.status, 200);
+ const meta = getMeta(await response.text());
+ assert.strictEqual(meta.clientName, 'Misklient');
+ assert.strictEqual(meta.clientLogo, `http://127.0.0.1:${clientPort}/logo.png`);
+ });
+
+ test('Missing Logo', async () => {
+ sender = (reply): void => {
+ reply.header('Link', '</redirect>; rel="redirect_uri"');
+ reply.send(`
+ <!DOCTYPE html>
+ <div class="h-app"><a href="/" class="u-url p-name">Misklient
+ `);
+ reply.send();
+ };
+
+ const client = new AuthorizationCode(clientConfig);
+
+ const response = await fetch(client.authorizeURL({
+ redirect_uri,
+ scope: 'write:notes',
+ state: 'state',
+ code_challenge: 'code',
+ code_challenge_method: 'S256',
+ } as AuthorizationParamsExtended));
+ assert.strictEqual(response.status, 200);
+ const meta = getMeta(await response.text());
+ assert.strictEqual(meta.clientName, 'Misklient');
+ assert.strictEqual(meta.clientLogo, undefined);
+ });
+
test('Mismatching URL in h-app', async () => {
sender = (reply): void => {
reply.header('Link', '</redirect>; rel="redirect_uri"');
diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts
index 1ac99df884..1edc178fc2 100644
--- a/packages/backend/test/e2e/thread-mute.ts
+++ b/packages/backend/test/e2e/thread-mute.ts
@@ -38,48 +38,6 @@ describe('Note thread mute', () => {
assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false);
});
- test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- const bobNote = await post(bob, { text: '@alice @carol root note' });
-
- await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
-
- const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
-
- const res = await api('i', {}, alice);
-
- assert.strictEqual(res.status, 200);
- assert.strictEqual(res.body.hasUnreadMentions, false);
- });
-
- test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise<void>(async done => {
- // 状態リセット
- await api('i/read-all-unread-notes', {}, alice);
-
- const bobNote = await post(bob, { text: '@alice @carol root note' });
-
- await api('notes/thread-muting/create', { noteId: bobNote.id }, alice);
-
- let fired = false;
-
- const ws = await connectStream(alice, 'main', async ({ type, body }) => {
- if (type === 'unreadMention') {
- if (body === bobNote.id) return;
- fired = true;
- }
- });
-
- const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
-
- setTimeout(() => {
- assert.strictEqual(fired, false);
- ws.close();
- done();
- }, 5000);
- }));
-
test('i/notifications にミュートしているスレッドの通知が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index 319c8581f4..d6d2cb33f0 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -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 * 15);
+ }, 1000 * 30);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 7b21834b57..58cfab23dc 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -15,7 +15,7 @@ describe('ユーザー', () => {
// エンティティとしてのユーザーを主眼においたテストを記述する
// (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする)
- const stripUndefined = <T extends { [key: string]: any }, >(orig: T): Partial<T> => {
+ const stripUndefined = <T extends { [key: string]: any } >(orig: T): Partial<T> => {
return Object.entries({ ...orig })
.filter(([, value]) => value !== undefined)
.reduce((obj: Partial<T>, [key, value]) => {
@@ -86,6 +86,7 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
+ chatScope: user.chatScope,
roles: user.roles,
memo: user.memo,
});
@@ -135,6 +136,7 @@ describe('ユーザー', () => {
hasUnreadAnnouncement: user.hasUnreadAnnouncement,
hasUnreadAntenna: user.hasUnreadAntenna,
hasUnreadChannel: user.hasUnreadChannel,
+ hasUnreadChatMessages: user.hasUnreadChatMessages,
hasUnreadNotification: user.hasUnreadNotification,
unreadNotificationsCount: user.unreadNotificationsCount,
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
@@ -350,6 +352,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
+ assert.strictEqual(response.chatScope, 'mutual');
assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null);
@@ -376,6 +379,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.hasUnreadAnnouncement, false);
assert.strictEqual(response.hasUnreadAntenna, false);
assert.strictEqual(response.hasUnreadChannel, false);
+ assert.strictEqual(response.hasUnreadChatMessages, false);
assert.strictEqual(response.hasUnreadNotification, false);
assert.strictEqual(response.unreadNotificationsCount, 0);
assert.strictEqual(response.hasPendingReceivedFollowRequest, false);
diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts
index d3302dc9bb..0bf85ef8eb 100644
--- a/packages/backend/test/misc/mock-resolver.ts
+++ b/packages/backend/test/misc/mock-resolver.ts
@@ -7,14 +7,10 @@ import type { Config } from '@/config.js';
import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import type { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import type { ApRequestService } from '@/core/activitypub/ApRequestService.js';
-import { Resolver } from '@/core/activitypub/ApResolverService.js';
import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
import type { HttpRequestService } from '@/core/HttpRequestService.js';
-import type { InstanceActorService } from '@/core/InstanceActorService.js';
import type { LoggerService } from '@/core/LoggerService.js';
-import type { MetaService } from '@/core/MetaService.js';
import type { UtilityService } from '@/core/UtilityService.js';
-import { bindThis } from '@/decorators.js';
import type {
FollowRequestsRepository,
MiMeta,
@@ -26,6 +22,9 @@ import type {
import { ApLogService } from '@/core/ApLogService.js';
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { fromTuple } from '@/misc/from-tuple.js';
+import { SystemAccountService } from '@/core/SystemAccountService.js';
+import { bindThis } from '@/decorators.js';
+import { Resolver } from '@/core/activitypub/ApResolverService.js';
type MockResponse = {
type: string;
@@ -46,7 +45,7 @@ export class MockResolver extends Resolver {
{} as NoteReactionsRepository,
{} as FollowRequestsRepository,
{} as UtilityService,
- {} as InstanceActorService,
+ {} as SystemAccountService,
{} as ApRequestService,
{} as HttpRequestService,
{} as ApRendererService,
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
index 1326003c5e..6d555326fb 100644
--- a/packages/backend/test/unit/AbuseReportNotificationService.ts
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -149,9 +149,9 @@ describe('AbuseReportNotificationService', () => {
});
beforeEach(async () => {
- 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 });
+ root = await createUser({ username: 'root', usernameLower: 'root' });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice' });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob' });
systemWebhook1 = await createWebhook();
systemWebhook2 = await createWebhook();
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
index 12ffaf3421..f2d9832f50 100644
--- a/packages/backend/test/unit/FlashService.ts
+++ b/packages/backend/test/unit/FlashService.ts
@@ -79,9 +79,9 @@ describe('FlashService', () => {
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 });
+ root = await createUser({ username: 'root', usernameLower: 'root' });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice' });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob' });
});
afterEach(async () => {
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index e54c006a4f..41480fdd51 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -24,13 +24,13 @@ describe('MfmService', () => {
describe('toHtml', () => {
test('br', () => {
const input = 'foo\nbar\nbaz';
- const output = '<p><span>foo<br>bar<br>baz</span></p>';
+ const output = '<p><span>foo<br />bar<br />baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
test('br alt', () => {
const input = 'foo\r\nbar\rbaz';
- const output = '<p><span>foo<br>bar<br>baz</span></p>';
+ const output = '<p><span>foo<br />bar<br />baz</span></p>';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts
index 9676abf07b..074430dd31 100644
--- a/packages/backend/test/unit/RelayService.ts
+++ b/packages/backend/test/unit/RelayService.ts
@@ -6,19 +6,18 @@
process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
-import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
-import { GlobalModule } from '@/GlobalModule.js';
-import { RelayService } from '@/core/RelayService.js';
+import { ModuleMocker } from 'jest-mock';
+import type { TestingModule } from '@nestjs/testing';
+import type { MockFunctionMetadata } from 'jest-mock';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
-import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { QueueService } from '@/core/QueueService.js';
import { IdService } from '@/core/IdService.js';
-import type { RelaysRepository } from '@/models/_.js';
-import { DI } from '@/di-symbols.js';
-import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
+import { QueueService } from '@/core/QueueService.js';
+import { RelayService } from '@/core/RelayService.js';
+import { SystemAccountService } from '@/core/SystemAccountService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { UtilityService } from '@/core/UtilityService.js';
const moduleMocker = new ModuleMocker(global);
@@ -26,8 +25,6 @@ describe('RelayService', () => {
let app: TestingModule;
let relayService: RelayService;
let queueService: jest.Mocked<QueueService>;
- let relaysRepository: RelaysRepository;
- let userEntityService: UserEntityService;
beforeAll(async () => {
app = await Test.createTestingModule({
@@ -36,10 +33,11 @@ describe('RelayService', () => {
],
providers: [
IdService,
- CreateSystemUserService,
ApRendererService,
RelayService,
UserEntityService,
+ SystemAccountService,
+ UtilityService,
],
})
.useMocker((token) => {
@@ -58,8 +56,6 @@ describe('RelayService', () => {
relayService = app.get<RelayService>(RelayService);
queueService = app.get<QueueService>(QueueService) as jest.Mocked<QueueService>;
- relaysRepository = app.get<RelaysRepository>(DI.relaysRepository);
- userEntityService = app.get<UserEntityService>(UserEntityService);
});
afterAll(async () => {
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index 9c1b1008d6..553ff0982a 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -57,6 +57,12 @@ describe('RoleService', () => {
return await usersRepository.findOneByOrFail(x.identifiers[0]);
}
+ async function createRoot(data: Partial<MiUser> = {}) {
+ const user = await createUser(data);
+ meta.rootUserId = user.id;
+ return user;
+ }
+
async function createRole(data: Partial<MiRole> = {}) {
const x = await rolesRepository.insert({
id: genAidx(Date.now()),
@@ -279,7 +285,7 @@ describe('RoleService', () => {
describe('getModeratorIds', () => {
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 }),
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -305,7 +311,7 @@ describe('RoleService', () => {
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 }),
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -331,7 +337,7 @@ describe('RoleService', () => {
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 }),
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -357,7 +363,7 @@ describe('RoleService', () => {
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 }),
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -383,7 +389,7 @@ describe('RoleService', () => {
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 }),
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -409,7 +415,7 @@ describe('RoleService', () => {
test('root has moderator role', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
- createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -433,7 +439,7 @@ describe('RoleService', () => {
test('root has administrator role', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
- createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -457,7 +463,7 @@ describe('RoleService', () => {
test('root has moderator role(expire)', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
- createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ createUser(), createUser(), createUser(), createRoot(),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
index fee4acb305..61187e9f2a 100644
--- a/packages/backend/test/unit/SystemWebhookService.ts
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -97,7 +97,7 @@ describe('SystemWebhookService', () => {
}
async function beforeEachImpl() {
- root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
+ root = await createUser({ username: 'root', usernameLower: 'root' });
}
async function afterEachImpl() {
diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts
index 7ea325d420..66a7f39ff1 100644
--- a/packages/backend/test/unit/UserSearchService.ts
+++ b/packages/backend/test/unit/UserSearchService.ts
@@ -113,7 +113,7 @@ describe('UserSearchService', () => {
});
beforeEach(async () => {
- root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ root = await createUser({ username: 'root', usernameLower: 'root' });
alice = await createUser({ username: 'Alice', usernameLower: 'alice' });
alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' });
alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' });
diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts
index db8f96df28..a2a85e9489 100644
--- a/packages/backend/test/unit/UserWebhookService.ts
+++ b/packages/backend/test/unit/UserWebhookService.ts
@@ -91,7 +91,7 @@ describe('UserWebhookService', () => {
}
async function beforeEachImpl() {
- root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
+ root = await createUser({ username: 'root', usernameLower: 'root' });
}
async function afterEachImpl() {
diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts
index be84ae9b84..736aac40b4 100644
--- a/packages/backend/test/unit/WebhookTestService.ts
+++ b/packages/backend/test/unit/WebhookTestService.ts
@@ -14,6 +14,7 @@ import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersReposi
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
describe('WebhookTestService', () => {
let app: TestingModule;
@@ -57,6 +58,11 @@ describe('WebhookTestService', () => {
WebhookTestService,
IdService,
{
+ provide: CustomEmojiService, useFactory: () => ({
+ populateEmojis: jest.fn(),
+ }),
+ },
+ {
provide: QueueService, useFactory: () => ({
systemWebhookDeliver: jest.fn(),
userWebhookDeliver: jest.fn(),
@@ -88,8 +94,8 @@ describe('WebhookTestService', () => {
});
beforeEach(async () => {
- root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
- alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+ root = await createUser({ username: 'root', usernameLower: 'root' });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice' });
userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
{ id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts
index d3d39240dc..f8b2a697f2 100644
--- a/packages/backend/test/unit/ap-request.ts
+++ b/packages/backend/test/unit/ap-request.ts
@@ -8,6 +8,8 @@ import httpSignature from '@peertube/http-signature';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
+import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
+import { IObject } from '@/core/activitypub/type.js';
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
return {
@@ -24,6 +26,10 @@ export const buildParsedSignature = (signingString: string, signature: string, a
};
};
+function cartesianProduct<T, U>(a: T[], b: U[]): [T, U][] {
+ return a.flatMap(a => b.map(b => [a, b] as [T, U]));
+}
+
describe('ap-request', () => {
test('createSignedPost with verify', async () => {
const keypair = await genRsaKeyPair();
@@ -58,4 +64,108 @@ describe('ap-request', () => {
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
assert.deepStrictEqual(result, true);
});
+
+ test('rejects non matching domain', () => {
+ assert.doesNotThrow(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://alice.example.com/abc' } as IObject,
+ 'https://alice.example.com/abc',
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should pass base case');
+ assert.throws(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://bob.example.com/abc' } as IObject,
+ 'https://alice.example.com/abc',
+ FetchAllowSoftFailMask.Any,
+ ), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
+
+ assert.doesNotThrow(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc#test',
+ { id: 'https://alice.example.com/abc' } as IObject,
+ 'https://alice.example.com/abc',
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should pass with hash in request URL');
+
+ // fix issues like threads
+ // https://github.com/misskey-dev/misskey/issues/15039
+ const withOrWithoutWWW = [
+ 'https://alice.example.com/abc',
+ 'https://www.alice.example.com/abc',
+ ];
+
+ cartesianProduct(
+ cartesianProduct(
+ withOrWithoutWWW,
+ withOrWithoutWWW,
+ ),
+ withOrWithoutWWW,
+ ).forEach(([[a, b], c]) => {
+ assert.doesNotThrow(() => assertActivityMatchesUrl(
+ a,
+ { id: b } as IObject,
+ c,
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should pass with or without www. subdomain');
+ });
+ });
+
+ test('cross origin lookup', () => {
+ assert.doesNotThrow(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://bob.example.com/abc' } as IObject,
+ 'https://bob.example.com/abc',
+ FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'validation should pass if the response is otherwise consistent and cross-origin is allowed');
+ assert.throws(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://bob.example.com/abc' } as IObject,
+ 'https://bob.example.com/abc',
+ FetchAllowSoftFailMask.Strict,
+ ), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
+ });
+
+ test('rejects non-canonical ID', () => {
+ assert.throws(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/@alice',
+ { id: 'https://alice.example.com/users/alice' } as IObject,
+ 'https://alice.example.com/users/alice',
+ FetchAllowSoftFailMask.Strict,
+ ), 'throws if the response ID did not exactly match the expected ID');
+ assert.doesNotThrow(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/@alice',
+ { id: 'https://alice.example.com/users/alice' } as IObject,
+ 'https://alice.example.com/users/alice',
+ FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'does not throw if non-canonical ID is allowed');
+ });
+
+ test('origin relaxed alignment', () => {
+ assert.doesNotThrow(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://ap.alice.example.com/abc' } as IObject,
+ 'https://ap.alice.example.com/abc',
+ FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'validation should pass if response is a subdomain of the expected origin');
+ assert.throws(() => assertActivityMatchesUrl(
+ 'https://alice.multi-tenant.example.com/abc',
+ { id: 'https://alice.multi-tenant.example.com/abc' } as IObject,
+ 'https://bob.multi-tenant.example.com/abc',
+ FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
+ ), 'validation should fail if response is a disjoint domain of the expected origin');
+ assert.throws(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://ap.alice.example.com/abc' } as IObject,
+ 'https://ap.alice.example.com/abc',
+ FetchAllowSoftFailMask.Strict,
+ ), 'throws if relaxed origin is forbidden');
+ });
+
+ test('resist HTTP downgrade', () => {
+ assert.throws(() => assertActivityMatchesUrl(
+ 'https://alice.example.com/abc',
+ { id: 'https://alice.example.com/abc' } as IObject,
+ 'http://alice.example.com/abc',
+ FetchAllowSoftFailMask.Strict,
+ ), 'throws if HTTP downgrade is detected');
+ });
});
diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts
index e4f42809f8..6b7eedff55 100644
--- a/packages/backend/test/unit/entities/UserEntityService.ts
+++ b/packages/backend/test/unit/entities/UserEntityService.ts
@@ -50,6 +50,7 @@ import { AccountMoveService } from '@/core/AccountMoveService.js';
import { ReactionService } from '@/core/ReactionService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { ChatService } from '@/core/ChatService.js';
process.env.NODE_ENV = 'test';
@@ -172,6 +173,7 @@ describe('UserEntityService', () => {
ReactionService,
ReactionsBufferingService,
NotificationService,
+ ChatService,
];
app = await Test.createTestingModule({
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
index d96e6b916a..07618e7762 100644
--- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -316,7 +316,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
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 }),
+ createUser({}, { email: 'root@example.com', emailVerified: true }),
]);
mockModeratorRole([user1, user2, user3, root]);
@@ -349,7 +349,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
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 }),
+ createUser({}, { email: 'root@example.com', emailVerified: true }),
]);
mockModeratorRole([user1, user2, user3, root]);
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index cf97473d14..8306208477 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -35,7 +35,7 @@ export type SystemWebhookPayload = {
createdAt: string;
type: string;
body: any;
-}
+};
const config = loadConfig();
export const port = config.port;
@@ -45,10 +45,6 @@ export const host = new URL(config.url).host;
export const WEBHOOK_HOST = 'http://localhost:15080';
export const WEBHOOK_PORT = 15080;
-export const cookie = (me: UserToken): string => {
- return `token=${me.token};`;
-};
-
export type ApiRequest<E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req'] = misskey.Endpoints[E]['req']> = {
endpoint: E,
parameters: P,