merge upstream
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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"');
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 { 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 { ModuleMocker } from 'jest-mock';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { MockFunctionMetadata } from 'jest-mock';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
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 () => {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
@@ -56,6 +57,11 @@ describe('WebhookTestService', () => {
|
||||
providers: [
|
||||
WebhookTestService,
|
||||
IdService,
|
||||
{
|
||||
provide: CustomEmojiService, useFactory: () => ({
|
||||
populateEmojis: jest.fn(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
provide: QueueService, useFactory: () => ({
|
||||
systemWebhookDeliver: 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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
+2
-2
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user