summaryrefslogtreecommitdiff
path: root/packages/backend/test/e2e
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-03-03 03:13:12 +0100
committerGitHub <noreply@github.com>2023-03-03 11:13:12 +0900
commit61215e50ff9e4c84787c8d99c75fd36dafbd8815 (patch)
tree36419e8a3ec97afa0a3a0011d523d80addf8e724 /packages/backend/test/e2e
parentfix(server): チャンネルでミュートが正しく機能していない... (diff)
downloadmisskey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.tar.gz
misskey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.tar.bz2
misskey-61215e50ff9e4c84787c8d99c75fd36dafbd8815.zip
test(backend): APIテストの復活 (#10163)
* Revert 1c5291f8185651c231903129ee7c1cee263f9f03 * APIテストの復活 * apiテストの移行 * moduleNameMapper修正 * simpleGetでthrowしないように status確認しているので要らない * longer timeout * ローカルでは問題ないのになんで * case sensitive * Run Nest instance within the current process * Skip some setIntervals * wait for 5 seconds * kill them all!! * logHeapUsage: true * detectOpenHandlesがじゃましているらしい * maxWorkers=1? * restore drive api tests * workerIdleMemoryLimit: 500MB * 1024MiB * Wait what
Diffstat (limited to 'packages/backend/test/e2e')
-rw-r--r--packages/backend/test/e2e/api-visibility.ts477
-rw-r--r--packages/backend/test/e2e/api.ts83
-rw-r--r--packages/backend/test/e2e/block.ts85
-rw-r--r--packages/backend/test/e2e/endpoints.ts797
-rw-r--r--packages/backend/test/e2e/fetch-resource.ts193
-rw-r--r--packages/backend/test/e2e/ff-visibility.ts165
-rw-r--r--packages/backend/test/e2e/mute.ts123
-rw-r--r--packages/backend/test/e2e/note.ts370
-rw-r--r--packages/backend/test/e2e/streaming.ts547
-rw-r--r--packages/backend/test/e2e/thread-mute.ts103
-rw-r--r--packages/backend/test/e2e/user-notes.ts61
11 files changed, 3004 insertions, 0 deletions
diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts
new file mode 100644
index 0000000000..4e162f42d0
--- /dev/null
+++ b/packages/backend/test/e2e/api-visibility.ts
@@ -0,0 +1,477 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, post, startServer } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('API visibility', () => {
+ let p: INestApplicationContext;
+
+ beforeAll(async () => {
+ p = await startServer();
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ describe('Note visibility', () => {
+ //#region vars
+ /** ヒロイン */
+ let alice: any;
+ /** フォロワー */
+ let follower: any;
+ /** 非フォロワー */
+ let other: any;
+ /** 非フォロワーでもリプライやメンションをされた人 */
+ let target: any;
+ /** specified mentionでmentionを飛ばされる人 */
+ let target2: any;
+
+ /** public-post */
+ let pub: any;
+ /** home-post */
+ let home: any;
+ /** followers-post */
+ let fol: any;
+ /** specified-post */
+ let spe: any;
+
+ /** public-reply to target's post */
+ let pubR: any;
+ /** home-reply to target's post */
+ let homeR: any;
+ /** followers-reply to target's post */
+ let folR: any;
+ /** specified-reply to target's post */
+ let speR: any;
+
+ /** public-mention to target */
+ let pubM: any;
+ /** home-mention to target */
+ let homeM: any;
+ /** followers-mention to target */
+ let folM: any;
+ /** specified-mention to target */
+ let speM: any;
+
+ /** reply target post */
+ let tgt: any;
+ //#endregion
+
+ const show = async (noteId: any, by: any) => {
+ return await api('/notes/show', {
+ noteId,
+ }, by);
+ };
+
+ beforeAll(async () => {
+ //#region prepare
+ // signup
+ alice = await signup({ username: 'alice' });
+ follower = await signup({ username: 'follower' });
+ other = await signup({ username: 'other' });
+ target = await signup({ username: 'target' });
+ target2 = await signup({ username: 'target2' });
+
+ // follow alice <= follower
+ await api('/following/create', { userId: alice.id }, follower);
+
+ // normal posts
+ pub = await post(alice, { text: 'x', visibility: 'public' });
+ home = await post(alice, { text: 'x', visibility: 'home' });
+ fol = await post(alice, { text: 'x', visibility: 'followers' });
+ spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
+
+ // replies
+ tgt = await post(target, { text: 'y', visibility: 'public' });
+ pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
+ homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
+ folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
+ speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
+
+ // mentions
+ pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
+ homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
+ folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
+ speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
+ //#endregion
+ });
+
+ //#region show post
+ // public
+ test('[show] public-postを自分が見れる', async () => {
+ const res = await show(pub.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-postをフォロワーが見れる', async () => {
+ const res = await show(pub.id, follower);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-postを非フォロワーが見れる', async () => {
+ const res = await show(pub.id, other);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-postを未認証が見れる', async () => {
+ const res = await show(pub.id, null);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ // home
+ test('[show] home-postを自分が見れる', async () => {
+ const res = await show(home.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-postをフォロワーが見れる', async () => {
+ const res = await show(home.id, follower);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-postを非フォロワーが見れる', async () => {
+ const res = await show(home.id, other);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-postを未認証が見れる', async () => {
+ const res = await show(home.id, null);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ // followers
+ test('[show] followers-postを自分が見れる', async () => {
+ const res = await show(fol.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] followers-postをフォロワーが見れる', async () => {
+ const res = await show(fol.id, follower);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] followers-postを非フォロワーが見れない', async () => {
+ const res = await show(fol.id, other);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] followers-postを未認証が見れない', async () => {
+ const res = await show(fol.id, null);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ // specified
+ test('[show] specified-postを自分が見れる', async () => {
+ const res = await show(spe.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] specified-postを指定ユーザーが見れる', async () => {
+ const res = await show(spe.id, target);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] specified-postをフォロワーが見れない', async () => {
+ const res = await show(spe.id, follower);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-postを非フォロワーが見れない', async () => {
+ const res = await show(spe.id, other);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-postを未認証が見れない', async () => {
+ const res = await show(spe.id, null);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+ //#endregion
+
+ //#region show reply
+ // public
+ test('[show] public-replyを自分が見れる', async () => {
+ const res = await show(pubR.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-replyをされた人が見れる', async () => {
+ const res = await show(pubR.id, target);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-replyをフォロワーが見れる', async () => {
+ const res = await show(pubR.id, follower);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-replyを非フォロワーが見れる', async () => {
+ const res = await show(pubR.id, other);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] public-replyを未認証が見れる', async () => {
+ const res = await show(pubR.id, null);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ // home
+ test('[show] home-replyを自分が見れる', async () => {
+ const res = await show(homeR.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-replyをされた人が見れる', async () => {
+ const res = await show(homeR.id, target);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-replyをフォロワーが見れる', async () => {
+ const res = await show(homeR.id, follower);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-replyを非フォロワーが見れる', async () => {
+ const res = await show(homeR.id, other);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] home-replyを未認証が見れる', async () => {
+ const res = await show(homeR.id, null);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ // followers
+ test('[show] followers-replyを自分が見れる', async () => {
+ const res = await show(folR.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async () => {
+ const res = await show(folR.id, target);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] followers-replyをフォロワーが見れる', async () => {
+ const res = await show(folR.id, follower);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] followers-replyを非フォロワーが見れない', async () => {
+ const res = await show(folR.id, other);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] followers-replyを未認証が見れない', async () => {
+ const res = await show(folR.id, null);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ // specified
+ test('[show] specified-replyを自分が見れる', async () => {
+ const res = await show(speR.id, alice);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] specified-replyを指定ユーザーが見れる', async () => {
+ const res = await show(speR.id, target);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] specified-replyをされた人が指定されてなくても見れる', async () => {
+ const res = await show(speR.id, target);
+ assert.strictEqual(res.body.text, 'x');
+ });
+
+ test('[show] specified-replyをフォロワーが見れない', async () => {
+ const res = await show(speR.id, follower);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-replyを非フォロワーが見れない', async () => {
+ const res = await show(speR.id, other);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-replyを未認証が見れない', async () => {
+ const res = await show(speR.id, null);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+ //#endregion
+
+ //#region show mention
+ // public
+ test('[show] public-mentionを自分が見れる', async () => {
+ const res = await show(pubM.id, alice);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] public-mentionをされた人が見れる', async () => {
+ const res = await show(pubM.id, target);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] public-mentionをフォロワーが見れる', async () => {
+ const res = await show(pubM.id, follower);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] public-mentionを非フォロワーが見れる', async () => {
+ const res = await show(pubM.id, other);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] public-mentionを未認証が見れる', async () => {
+ const res = await show(pubM.id, null);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ // home
+ test('[show] home-mentionを自分が見れる', async () => {
+ const res = await show(homeM.id, alice);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] home-mentionをされた人が見れる', async () => {
+ const res = await show(homeM.id, target);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] home-mentionをフォロワーが見れる', async () => {
+ const res = await show(homeM.id, follower);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] home-mentionを非フォロワーが見れる', async () => {
+ const res = await show(homeM.id, other);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] home-mentionを未認証が見れる', async () => {
+ const res = await show(homeM.id, null);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ // followers
+ test('[show] followers-mentionを自分が見れる', async () => {
+ const res = await show(folM.id, alice);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] followers-mentionをメンションされていれば非フォロワーでも見れる', async () => {
+ const res = await show(folM.id, target);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] followers-mentionをフォロワーが見れる', async () => {
+ const res = await show(folM.id, follower);
+ assert.strictEqual(res.body.text, '@target x');
+ });
+
+ test('[show] followers-mentionを非フォロワーが見れない', async () => {
+ const res = await show(folM.id, other);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] followers-mentionを未認証が見れない', async () => {
+ const res = await show(folM.id, null);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ // specified
+ test('[show] specified-mentionを自分が見れる', async () => {
+ const res = await show(speM.id, alice);
+ assert.strictEqual(res.body.text, '@target2 x');
+ });
+
+ test('[show] specified-mentionを指定ユーザーが見れる', async () => {
+ const res = await show(speM.id, target);
+ assert.strictEqual(res.body.text, '@target2 x');
+ });
+
+ test('[show] specified-mentionをされた人が指定されてなかったら見れない', async () => {
+ const res = await show(speM.id, target2);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-mentionをフォロワーが見れない', async () => {
+ const res = await show(speM.id, follower);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-mentionを非フォロワーが見れない', async () => {
+ const res = await show(speM.id, other);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+
+ test('[show] specified-mentionを未認証が見れない', async () => {
+ const res = await show(speM.id, null);
+ assert.strictEqual(res.body.isHidden, true);
+ });
+ //#endregion
+
+ //#region HTL
+ test('[HTL] public-post が 自分が見れる', async () => {
+ const res = await api('/notes/timeline', { limit: 100 }, alice);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === pub.id);
+ assert.strictEqual(notes[0].text, 'x');
+ });
+
+ test('[HTL] public-post が 非フォロワーから見れない', async () => {
+ const res = await api('/notes/timeline', { limit: 100 }, other);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === pub.id);
+ assert.strictEqual(notes.length, 0);
+ });
+
+ test('[HTL] followers-post が フォロワーから見れる', async () => {
+ const res = await api('/notes/timeline', { limit: 100 }, follower);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === fol.id);
+ assert.strictEqual(notes[0].text, 'x');
+ });
+ //#endregion
+
+ //#region RTL
+ test('[replies] followers-reply が フォロワーから見れる', async () => {
+ const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === folR.id);
+ assert.strictEqual(notes[0].text, 'x');
+ });
+
+ test('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async () => {
+ const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === folR.id);
+ assert.strictEqual(notes.length, 0);
+ });
+
+ test('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
+ const res = await api('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === folR.id);
+ assert.strictEqual(notes[0].text, 'x');
+ });
+ //#endregion
+
+ //#region MTL
+ test('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async () => {
+ const res = await api('/notes/mentions', { limit: 100 }, target);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === folR.id);
+ assert.strictEqual(notes[0].text, 'x');
+ });
+
+ test('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async () => {
+ const res = await api('/notes/mentions', { limit: 100 }, target);
+ assert.strictEqual(res.status, 200);
+ const notes = res.body.filter((n: any) => n.id === folM.id);
+ assert.strictEqual(notes[0].text, '@target x');
+ });
+ //#endregion
+ });
+});
+
diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts
new file mode 100644
index 0000000000..6ceccf66eb
--- /dev/null
+++ b/packages/backend/test/e2e/api.ts
@@ -0,0 +1,83 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, startServer } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('API', () => {
+ let p: INestApplicationContext;
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ describe('General validation', () => {
+ test('wrong type', async () => {
+ const res = await api('/test', {
+ required: true,
+ string: 42,
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('missing require param', async () => {
+ const res = await api('/test', {
+ string: 'a',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('invalid misskey:id (empty string)', async () => {
+ const res = await api('/test', {
+ required: true,
+ id: '',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('valid misskey:id', async () => {
+ const res = await api('/test', {
+ required: true,
+ id: '8wvhjghbxu',
+ });
+ assert.strictEqual(res.status, 200);
+ });
+
+ test('default value', async () => {
+ const res = await api('/test', {
+ required: true,
+ string: 'a',
+ });
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.body.default, 'hello');
+ });
+
+ test('can set null even if it has default value', async () => {
+ const res = await api('/test', {
+ required: true,
+ nullableDefault: null,
+ });
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.body.nullableDefault, null);
+ });
+
+ test('cannot set undefined if it has default value', async () => {
+ const res = await api('/test', {
+ required: true,
+ nullableDefault: undefined,
+ });
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.body.nullableDefault, 'hello');
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts
new file mode 100644
index 0000000000..4e9030f85d
--- /dev/null
+++ b/packages/backend/test/e2e/block.ts
@@ -0,0 +1,85 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, post, startServer } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Block', () => {
+ let p: INestApplicationContext;
+
+ // alice blocks bob
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ test('Block作成', async () => {
+ const res = await api('/blocking/create', {
+ userId: bob.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ });
+
+ test('ブロックされているユーザーをフォローできない', async () => {
+ const res = await api('/following/create', { userId: alice.id }, bob);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0');
+ });
+
+ test('ブロックされているユーザーにリアクションできない', async () => {
+ const note = await post(alice, { text: 'hello' });
+
+ const res = await api('/notes/reactions/create', { noteId: note.id, reaction: '👍' }, bob);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec');
+ });
+
+ test('ブロックされているユーザーに返信できない', async () => {
+ const note = await post(alice, { text: 'hello' });
+
+ const res = await api('/notes/create', { replyId: note.id, text: 'yo' }, bob);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
+ });
+
+ test('ブロックされているユーザーのノートをRenoteできない', async () => {
+ const note = await post(alice, { text: 'hello' });
+
+ const res = await api('/notes/create', { renoteId: note.id, text: 'yo' }, bob);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3');
+ });
+
+ // TODO: ユーザーリストに入れられないテスト
+
+ // TODO: ユーザーリストから除外されるテスト
+
+ test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
+ const aliceNote = await post(alice);
+ const bobNote = await post(bob);
+ const carolNote = await post(carol);
+
+ const res = await api('/notes/local-timeline', {}, bob);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), false);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+ });
+});
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
new file mode 100644
index 0000000000..e864eab6cb
--- /dev/null
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -0,0 +1,797 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+// node-fetch only supports it's own Blob yet
+// https://github.com/node-fetch/node-fetch/pull/1664
+import { Blob } from 'node-fetch';
+import { startServer, signup, post, api, uploadFile } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Endpoints', () => {
+ let p: INestApplicationContext;
+
+ let alice: any;
+ let bob: any;
+ let carol: any;
+ let dave: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ dave = await signup({ username: 'dave' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ describe('signup', () => {
+ test('不正なユーザー名でアカウントが作成できない', async () => {
+ const res = await api('signup', {
+ username: 'test.',
+ password: 'test',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('空のパスワードでアカウントが作成できない', async () => {
+ const res = await api('signup', {
+ username: 'test',
+ password: '',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('正しくアカウントが作成できる', async () => {
+ const me = {
+ username: 'test1',
+ password: 'test1',
+ };
+
+ const res = await api('signup', me);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.username, me.username);
+ });
+
+ test('同じユーザー名のアカウントは作成できない', async () => {
+ const res = await api('signup', {
+ username: 'test1',
+ password: 'test1',
+ });
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('signin', () => {
+ test('間違ったパスワードでサインインできない', async () => {
+ const res = await api('signin', {
+ username: 'test1',
+ password: 'bar',
+ });
+
+ assert.strictEqual(res.status, 403);
+ });
+
+ test('クエリをインジェクションできない', async () => {
+ const res = await api('signin', {
+ username: 'test1',
+ password: {
+ $gt: '',
+ },
+ });
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('正しい情報でサインインできる', async () => {
+ const res = await api('signin', {
+ username: 'test1',
+ password: 'test1',
+ });
+
+ assert.strictEqual(res.status, 200);
+ });
+ });
+
+ describe('i/update', () => {
+ test('アカウント設定を更新できる', async () => {
+ const myName = '大室櫻子';
+ const myLocation = '七森中';
+ const myBirthday = '2000-09-07';
+
+ const res = await api('/i/update', {
+ name: myName,
+ location: myLocation,
+ birthday: myBirthday,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, myName);
+ assert.strictEqual(res.body.location, myLocation);
+ assert.strictEqual(res.body.birthday, myBirthday);
+ });
+
+ test('名前を空白にできる', async () => {
+ const res = await api('/i/update', {
+ name: ' ',
+ }, alice);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.body.name, ' ');
+ });
+
+ test('誕生日の設定を削除できる', async () => {
+ await api('/i/update', {
+ birthday: '2000-09-07',
+ }, alice);
+
+ const res = await api('/i/update', {
+ birthday: null,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.birthday, null);
+ });
+
+ test('不正な誕生日の形式で怒られる', async () => {
+ const res = await api('/i/update', {
+ birthday: '2000/09/07',
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('users/show', () => {
+ test('ユーザーが取得できる', async () => {
+ const res = await api('/users/show', {
+ userId: alice.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.id, alice.id);
+ });
+
+ test('ユーザーが存在しなかったら怒る', async () => {
+ const res = await api('/users/show', {
+ userId: '000000000000000000000000',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('間違ったIDで怒られる', async () => {
+ const res = await api('/users/show', {
+ userId: 'kyoppie',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('notes/show', () => {
+ test('投稿が取得できる', async () => {
+ const myPost = await post(alice, {
+ text: 'test',
+ });
+
+ const res = await api('/notes/show', {
+ noteId: myPost.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.id, myPost.id);
+ assert.strictEqual(res.body.text, myPost.text);
+ });
+
+ test('投稿が存在しなかったら怒る', async () => {
+ const res = await api('/notes/show', {
+ noteId: '000000000000000000000000',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('間違ったIDで怒られる', async () => {
+ const res = await api('/notes/show', {
+ noteId: 'kyoppie',
+ });
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('notes/reactions/create', () => {
+ test('リアクションできる', async () => {
+ const bobPost = await post(bob);
+
+ const res = await api('/notes/reactions/create', {
+ noteId: bobPost.id,
+ reaction: '🚀',
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+
+ const resNote = await api('/notes/show', {
+ noteId: bobPost.id,
+ }, alice);
+
+ assert.strictEqual(resNote.status, 200);
+ assert.strictEqual(resNote.body.reactions['🚀'], 1);
+ });
+
+ test('自分の投稿にもリアクションできる', async () => {
+ const myPost = await post(alice);
+
+ const res = await api('/notes/reactions/create', {
+ noteId: myPost.id,
+ reaction: '🚀',
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ });
+
+ test('二重にリアクションすると上書きされる', async () => {
+ const bobPost = await post(bob);
+
+ await api('/notes/reactions/create', {
+ noteId: bobPost.id,
+ reaction: '🥰',
+ }, alice);
+
+ const res = await api('/notes/reactions/create', {
+ noteId: bobPost.id,
+ reaction: '🚀',
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+
+ const resNote = await api('/notes/show', {
+ noteId: bobPost.id,
+ }, alice);
+
+ assert.strictEqual(resNote.status, 200);
+ assert.deepStrictEqual(resNote.body.reactions, { '🚀': 1 });
+ });
+
+ test('存在しない投稿にはリアクションできない', async () => {
+ const res = await api('/notes/reactions/create', {
+ noteId: '000000000000000000000000',
+ reaction: '🚀',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('空のパラメータで怒られる', async () => {
+ const res = await api('/notes/reactions/create', {}, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('間違ったIDで怒られる', async () => {
+ const res = await api('/notes/reactions/create', {
+ noteId: 'kyoppie',
+ reaction: '🚀',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('following/create', () => {
+ test('フォローできる', async () => {
+ const res = await api('/following/create', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(res.status, 200);
+ });
+
+ test('既にフォローしている場合は怒る', async () => {
+ const res = await api('/following/create', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないユーザーはフォローできない', async () => {
+ const res = await api('/following/create', {
+ userId: '000000000000000000000000',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('自分自身はフォローできない', async () => {
+ const res = await api('/following/create', {
+ userId: alice.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('空のパラメータで怒られる', async () => {
+ const res = await api('/following/create', {}, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('間違ったIDで怒られる', async () => {
+ const res = await api('/following/create', {
+ userId: 'foo',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('following/delete', () => {
+ test('フォロー解除できる', async () => {
+ await api('/following/create', {
+ userId: alice.id,
+ }, bob);
+
+ const res = await api('/following/delete', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(res.status, 200);
+ });
+
+ test('フォローしていない場合は怒る', async () => {
+ const res = await api('/following/delete', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないユーザーはフォロー解除できない', async () => {
+ const res = await api('/following/delete', {
+ userId: '000000000000000000000000',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('自分自身はフォロー解除できない', async () => {
+ const res = await api('/following/delete', {
+ userId: alice.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('空のパラメータで怒られる', async () => {
+ const res = await api('/following/delete', {}, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('間違ったIDで怒られる', async () => {
+ const res = await api('/following/delete', {
+ userId: 'kyoppie',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('drive', () => {
+ test('ドライブ情報を取得できる', async () => {
+ await uploadFile(alice, {
+ blob: new Blob([new Uint8Array(256)]),
+ });
+ await uploadFile(alice, {
+ blob: new Blob([new Uint8Array(512)]),
+ });
+ await uploadFile(alice, {
+ blob: new Blob([new Uint8Array(1024)]),
+ });
+ const res = await api('/drive', {}, alice);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ expect(res.body).toHaveProperty('usage', 1792);
+ });
+ });
+
+ describe('drive/files/create', () => {
+ test('ファイルを作成できる', async () => {
+ const res = await uploadFile(alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'Lenna.jpg');
+ });
+
+ test('ファイルに名前を付けられる', async () => {
+ const res = await uploadFile(alice, { name: 'Belmond.png' });
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'Belmond.png');
+ });
+
+ test('ファイル無しで怒られる', async () => {
+ const res = await api('/drive/files/create', {}, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('SVGファイルを作成できる', async () => {
+ const res = await uploadFile(alice, { path: 'image.svg' });
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'image.svg');
+ assert.strictEqual(res.body.type, 'image/svg+xml');
+ });
+ });
+
+ describe('drive/files/update', () => {
+ test('名前を更新できる', async () => {
+ const file = (await uploadFile(alice)).body;
+ const newName = 'いちごパスタ.png';
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ name: newName,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, newName);
+ });
+
+ test('他人のファイルは更新できない', async () => {
+ const file = (await uploadFile(alice)).body;
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ name: 'いちごパスタ.png',
+ }, bob);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('親フォルダを更新できる', async () => {
+ const file = (await uploadFile(alice)).body;
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ folderId: folder.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.folderId, folder.id);
+ });
+
+ test('親フォルダを無しにできる', async () => {
+ const file = (await uploadFile(alice)).body;
+
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+
+ await api('/drive/files/update', {
+ fileId: file.id,
+ folderId: folder.id,
+ }, alice);
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ folderId: null,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.folderId, null);
+ });
+
+ test('他人のフォルダには入れられない', async () => {
+ const file = (await uploadFile(alice)).body;
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, bob)).body;
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ folderId: folder.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないフォルダで怒られる', async () => {
+ const file = (await uploadFile(alice)).body;
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ folderId: '000000000000000000000000',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('不正なフォルダIDで怒られる', async () => {
+ const file = (await uploadFile(alice)).body;
+
+ const res = await api('/drive/files/update', {
+ fileId: file.id,
+ folderId: 'foo',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('ファイルが存在しなかったら怒る', async () => {
+ const res = await api('/drive/files/update', {
+ fileId: '000000000000000000000000',
+ name: 'いちごパスタ.png',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('間違ったIDで怒られる', async () => {
+ const res = await api('/drive/files/update', {
+ fileId: 'kyoppie',
+ name: 'いちごパスタ.png',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('drive/folders/create', () => {
+ test('フォルダを作成できる', async () => {
+ const res = await api('/drive/folders/create', {
+ name: 'test',
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'test');
+ });
+ });
+
+ describe('drive/folders/update', () => {
+ test('名前を更新できる', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ name: 'new name',
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.name, 'new name');
+ });
+
+ test('他人のフォルダを更新できない', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, bob)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ name: 'new name',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('親フォルダを更新できる', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ const parentFolder = (await api('/drive/folders/create', {
+ name: 'parent',
+ }, alice)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: parentFolder.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.parentId, parentFolder.id);
+ });
+
+ test('親フォルダを無しに更新できる', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ const parentFolder = (await api('/drive/folders/create', {
+ name: 'parent',
+ }, alice)).body;
+ await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: parentFolder.id,
+ }, alice);
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: null,
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.parentId, null);
+ });
+
+ test('他人のフォルダを親フォルダに設定できない', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ const parentFolder = (await api('/drive/folders/create', {
+ name: 'parent',
+ }, bob)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: parentFolder.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('フォルダが循環するような構造にできない', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ const parentFolder = (await api('/drive/folders/create', {
+ name: 'parent',
+ }, alice)).body;
+ await api('/drive/folders/update', {
+ folderId: parentFolder.id,
+ parentId: folder.id,
+ }, alice);
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: parentFolder.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('フォルダが循環するような構造にできない(再帰的)', async () => {
+ const folderA = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ const folderB = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ const folderC = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+ await api('/drive/folders/update', {
+ folderId: folderB.id,
+ parentId: folderA.id,
+ }, alice);
+ await api('/drive/folders/update', {
+ folderId: folderC.id,
+ parentId: folderB.id,
+ }, alice);
+
+ const res = await api('/drive/folders/update', {
+ folderId: folderA.id,
+ parentId: folderC.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('フォルダが循環するような構造にできない(自身)', async () => {
+ const folderA = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folderA.id,
+ parentId: folderA.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しない親フォルダを設定できない', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: '000000000000000000000000',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('不正な親フォルダIDで怒られる', async () => {
+ const folder = (await api('/drive/folders/create', {
+ name: 'test',
+ }, alice)).body;
+
+ const res = await api('/drive/folders/update', {
+ folderId: folder.id,
+ parentId: 'foo',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないフォルダを更新できない', async () => {
+ const res = await api('/drive/folders/update', {
+ folderId: '000000000000000000000000',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('不正なフォルダIDで怒られる', async () => {
+ const res = await api('/drive/folders/update', {
+ folderId: 'foo',
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('notes/replies', () => {
+ test('自分に閲覧権限のない投稿は含まれない', async () => {
+ const alicePost = await post(alice, {
+ text: 'foo',
+ });
+
+ await post(bob, {
+ replyId: alicePost.id,
+ text: 'bar',
+ visibility: 'specified',
+ visibleUserIds: [alice.id],
+ });
+
+ const res = await api('/notes/replies', {
+ noteId: alicePost.id,
+ }, carol);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 0);
+ });
+ });
+
+ describe('notes/timeline', () => {
+ test('フォロワー限定投稿が含まれる', async () => {
+ await api('/following/create', {
+ userId: carol.id,
+ }, dave);
+
+ const carolPost = await post(carol, {
+ text: 'foo',
+ visibility: 'followers',
+ });
+
+ const res = await api('/notes/timeline', {}, dave);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 1);
+ assert.strictEqual(res.body[0].id, carolPost.id);
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts
new file mode 100644
index 0000000000..6b3c795235
--- /dev/null
+++ b/packages/backend/test/e2e/fetch-resource.ts
@@ -0,0 +1,193 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { startServer, signup, post, api, simpleGet } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+// Request Accept
+const ONLY_AP = 'application/activity+json';
+const PREFER_AP = 'application/activity+json, */*';
+const PREFER_HTML = 'text/html, */*';
+const UNSPECIFIED = '*/*';
+
+// Response Content-Type
+const AP = 'application/activity+json; charset=utf-8';
+const HTML = 'text/html; charset=utf-8';
+
+describe('Fetch resource', () => {
+ let p: INestApplicationContext;
+
+ let alice: any;
+ let alicesPost: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ alicesPost = await post(alice, {
+ text: 'test',
+ });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ describe('Common', () => {
+ test('meta', async () => {
+ const res = await api('/meta', {
+ });
+
+ assert.strictEqual(res.status, 200);
+ });
+
+ test('GET root', async () => {
+ const res = await simpleGet('/');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, HTML);
+ });
+
+ test('GET docs', async () => {
+ const res = await simpleGet('/docs/ja-JP/about');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, HTML);
+ });
+
+ test('GET api-doc (廃止)', async () => {
+ const res = await simpleGet('/api-doc');
+ assert.strictEqual(res.status, 404);
+ });
+
+ test('GET api.json (廃止)', async () => {
+ const res = await simpleGet('/api.json');
+ assert.strictEqual(res.status, 404);
+ });
+
+ test('GET api/foo (存在しない)', async () => {
+ const res = await simpleGet('/api/foo');
+ assert.strictEqual(res.status, 404);
+ assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT');
+ });
+
+ test('GET favicon.ico', async () => {
+ const res = await simpleGet('/favicon.ico');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'image/vnd.microsoft.icon');
+ });
+
+ test('GET apple-touch-icon.png', async () => {
+ const res = await simpleGet('/apple-touch-icon.png');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'image/png');
+ });
+
+ test('GET twemoji svg', async () => {
+ const res = await simpleGet('/twemoji/2764.svg');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'image/svg+xml');
+ });
+
+ test('GET twemoji svg with hyphen', async () => {
+ const res = await simpleGet('/twemoji/2764-fe0f-200d-1f525.svg');
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'image/svg+xml');
+ });
+ });
+
+ describe('/@:username', () => {
+ test('Only AP => AP', async () => {
+ const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, AP);
+ });
+
+ test('Prefer AP => AP', async () => {
+ const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, AP);
+ });
+
+ test('Prefer HTML => HTML', async () => {
+ const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, HTML);
+ });
+
+ test('Unspecified => HTML', async () => {
+ const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, HTML);
+ });
+ });
+
+ describe('/users/:id', () => {
+ test('Only AP => AP', async () => {
+ const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, AP);
+ });
+
+ test('Prefer AP => AP', async () => {
+ const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, AP);
+ });
+
+ test('Prefer HTML => Redirect to /@:username', async () => {
+ const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
+ assert.strictEqual(res.status, 302);
+ assert.strictEqual(res.location, `/@${alice.username}`);
+ });
+
+ test('Undecided => HTML', async () => {
+ const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
+ assert.strictEqual(res.status, 302);
+ assert.strictEqual(res.location, `/@${alice.username}`);
+ });
+ });
+
+ describe('/notes/:id', () => {
+ test('Only AP => AP', async () => {
+ const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, AP);
+ });
+
+ test('Prefer AP => AP', async () => {
+ const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, AP);
+ });
+
+ test('Prefer HTML => HTML', async () => {
+ const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, HTML);
+ });
+
+ test('Unspecified => HTML', async () => {
+ const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, HTML);
+ });
+ });
+
+ describe('Feeds', () => {
+ test('RSS', async () => {
+ const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'application/rss+xml; charset=utf-8');
+ });
+
+ test('ATOM', async () => {
+ const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'application/atom+xml; charset=utf-8');
+ });
+
+ test('JSON', async () => {
+ const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.type, 'application/json; charset=utf-8');
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts
new file mode 100644
index 0000000000..d53919ca1e
--- /dev/null
+++ b/packages/backend/test/e2e/ff-visibility.ts
@@ -0,0 +1,165 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, startServer, simpleGet } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('FF visibility', () => {
+ let p: INestApplicationContext;
+
+ let alice: any;
+ let bob: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
+ await api('/i/update', {
+ ffVisibility: 'public',
+ }, alice);
+
+ const followingRes = await api('/users/following', {
+ userId: alice.id,
+ }, bob);
+ const followersRes = await api('/users/followers', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(followingRes.status, 200);
+ assert.strictEqual(Array.isArray(followingRes.body), true);
+ assert.strictEqual(followersRes.status, 200);
+ assert.strictEqual(Array.isArray(followersRes.body), true);
+ });
+
+ test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
+ await api('/i/update', {
+ ffVisibility: 'followers',
+ }, alice);
+
+ const followingRes = await api('/users/following', {
+ userId: alice.id,
+ }, alice);
+ const followersRes = await api('/users/followers', {
+ userId: alice.id,
+ }, alice);
+
+ assert.strictEqual(followingRes.status, 200);
+ assert.strictEqual(Array.isArray(followingRes.body), true);
+ assert.strictEqual(followersRes.status, 200);
+ assert.strictEqual(Array.isArray(followersRes.body), true);
+ });
+
+ test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
+ await api('/i/update', {
+ ffVisibility: 'followers',
+ }, alice);
+
+ const followingRes = await api('/users/following', {
+ userId: alice.id,
+ }, bob);
+ const followersRes = await api('/users/followers', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(followingRes.status, 400);
+ assert.strictEqual(followersRes.status, 400);
+ });
+
+ test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
+ await api('/i/update', {
+ ffVisibility: 'followers',
+ }, alice);
+
+ await api('/following/create', {
+ userId: alice.id,
+ }, bob);
+
+ const followingRes = await api('/users/following', {
+ userId: alice.id,
+ }, bob);
+ const followersRes = await api('/users/followers', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(followingRes.status, 200);
+ assert.strictEqual(Array.isArray(followingRes.body), true);
+ assert.strictEqual(followersRes.status, 200);
+ assert.strictEqual(Array.isArray(followersRes.body), true);
+ });
+
+ test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
+ await api('/i/update', {
+ ffVisibility: 'private',
+ }, alice);
+
+ const followingRes = await api('/users/following', {
+ userId: alice.id,
+ }, alice);
+ const followersRes = await api('/users/followers', {
+ userId: alice.id,
+ }, alice);
+
+ assert.strictEqual(followingRes.status, 200);
+ assert.strictEqual(Array.isArray(followingRes.body), true);
+ assert.strictEqual(followersRes.status, 200);
+ assert.strictEqual(Array.isArray(followersRes.body), true);
+ });
+
+ test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
+ await api('/i/update', {
+ ffVisibility: 'private',
+ }, alice);
+
+ const followingRes = await api('/users/following', {
+ userId: alice.id,
+ }, bob);
+ const followersRes = await api('/users/followers', {
+ userId: alice.id,
+ }, bob);
+
+ assert.strictEqual(followingRes.status, 400);
+ assert.strictEqual(followersRes.status, 400);
+ });
+
+ describe('AP', () => {
+ test('ffVisibility が public 以外ならばAPからは取得できない', async () => {
+ {
+ await api('/i/update', {
+ ffVisibility: 'public',
+ }, alice);
+
+ const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
+ const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
+ assert.strictEqual(followingRes.status, 200);
+ assert.strictEqual(followersRes.status, 200);
+ }
+ {
+ await api('/i/update', {
+ ffVisibility: 'followers',
+ }, alice);
+
+ const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
+ const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
+ assert.strictEqual(followingRes.status, 403);
+ assert.strictEqual(followersRes.status, 403);
+ }
+ {
+ await api('/i/update', {
+ ffVisibility: 'private',
+ }, alice);
+
+ const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
+ const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
+ assert.strictEqual(followingRes.status, 403);
+ assert.strictEqual(followersRes.status, 403);
+ }
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
new file mode 100644
index 0000000000..6654a290be
--- /dev/null
+++ b/packages/backend/test/e2e/mute.ts
@@ -0,0 +1,123 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, post, react, startServer, waitFire } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Mute', () => {
+ let p: INestApplicationContext;
+
+ // alice mutes carol
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ test('ミュート作成', async () => {
+ const res = await api('/mute/create', {
+ userId: carol.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ });
+
+ test('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async () => {
+ const bobNote = await post(bob, { text: '@alice hi' });
+ const carolNote = await post(carol, { text: '@alice hi' });
+
+ const res = await api('/notes/mentions', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => 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');
+
+ assert.strictEqual(fired, false);
+ });
+
+ describe('Timeline', () => {
+ test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
+ const aliceNote = await post(alice);
+ const bobNote = await post(bob);
+ const carolNote = await post(carol);
+
+ const res = await api('/notes/local-timeline', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+ });
+
+ test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
+ const aliceNote = await post(alice);
+ const carolNote = await post(carol);
+ const bobNote = await post(bob, {
+ renoteId: carolNote.id,
+ });
+
+ const res = await api('/notes/local-timeline', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
+ });
+ });
+
+ describe('Notification', () => {
+ test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
+ const aliceNote = await post(alice);
+ await react(bob, aliceNote, 'like');
+ await react(carol, aliceNote, 'like');
+
+ const res = await api('/i/notifications', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true);
+ assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false);
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts
new file mode 100644
index 0000000000..98ee34d8d1
--- /dev/null
+++ b/packages/backend/test/e2e/note.ts
@@ -0,0 +1,370 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { Note } from '@/models/entities/Note.js';
+import { signup, post, uploadUrl, startServer, initTestDb, api } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Note', () => {
+ let p: INestApplicationContext;
+ let Notes: any;
+
+ let alice: any;
+ let bob: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ const connection = await initTestDb(true);
+ Notes = connection.getRepository(Note);
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ test('投稿できる', async () => {
+ const post = {
+ text: 'test',
+ };
+
+ const res = await api('/notes/create', post, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, post.text);
+ });
+
+ test('ファイルを添付できる', async () => {
+ const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
+
+ const res = await api('/notes/create', {
+ fileIds: [file.id],
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
+ }, 1000 * 10);
+
+ test('他人のファイルで怒られる', async () => {
+ const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
+
+ const res = await api('/notes/create', {
+ text: 'test',
+ fileIds: [file.id],
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
+ assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
+ }, 1000 * 10);
+
+ test('存在しないファイルで怒られる', async () => {
+ const res = await api('/notes/create', {
+ text: 'test',
+ fileIds: ['000000000000000000000000'],
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
+ assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
+ });
+
+ test('不正なファイルIDで怒られる', async () => {
+ const res = await api('/notes/create', {
+ fileIds: ['kyoppie'],
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE');
+ assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306');
+ });
+
+ test('返信できる', async () => {
+ const bobPost = await post(bob, {
+ text: 'foo',
+ });
+
+ const alicePost = {
+ text: 'bar',
+ replyId: bobPost.id,
+ };
+
+ const res = await api('/notes/create', alicePost, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, alicePost.text);
+ assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
+ assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
+ });
+
+ test('renoteできる', async () => {
+ const bobPost = await post(bob, {
+ text: 'test',
+ });
+
+ const alicePost = {
+ renoteId: bobPost.id,
+ };
+
+ const res = await api('/notes/create', alicePost, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+ assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
+ });
+
+ test('引用renoteできる', async () => {
+ const bobPost = await post(bob, {
+ text: 'test',
+ });
+
+ const alicePost = {
+ text: 'test',
+ renoteId: bobPost.id,
+ };
+
+ const res = await api('/notes/create', alicePost, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, alicePost.text);
+ assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
+ assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
+ });
+
+ test('文字数ぎりぎりで怒られない', async () => {
+ const post = {
+ text: '!'.repeat(3000),
+ };
+ const res = await api('/notes/create', post, alice);
+ assert.strictEqual(res.status, 200);
+ });
+
+ test('文字数オーバーで怒られる', async () => {
+ const post = {
+ text: '!'.repeat(3001),
+ };
+ const res = await api('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないリプライ先で怒られる', async () => {
+ const post = {
+ text: 'test',
+ replyId: '000000000000000000000000',
+ };
+ const res = await api('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないrenote対象で怒られる', async () => {
+ const post = {
+ renoteId: '000000000000000000000000',
+ };
+ const res = await api('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('不正なリプライ先IDで怒られる', async () => {
+ const post = {
+ text: 'test',
+ replyId: 'foo',
+ };
+ const res = await api('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('不正なrenote対象IDで怒られる', async () => {
+ const post = {
+ renoteId: 'foo',
+ };
+ const res = await api('/notes/create', post, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('存在しないユーザーにメンションできる', async () => {
+ const post = {
+ text: '@ghost yo',
+ };
+
+ const res = await api('/notes/create', post, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, post.text);
+ });
+
+ test('同じユーザーに複数メンションしても内部的にまとめられる', async () => {
+ const post = {
+ text: '@bob @bob @bob yo',
+ };
+
+ const res = await api('/notes/create', post, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.text, post.text);
+
+ const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
+ assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
+ });
+
+ describe('notes/create', () => {
+ test('投票を添付できる', async () => {
+ const res = await api('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['foo', 'bar'],
+ },
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
+ assert.strictEqual(res.body.createdNote.poll != null, true);
+ });
+
+ test('投票の選択肢が無くて怒られる', async () => {
+ const res = await api('/notes/create', {
+ poll: {},
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('投票の選択肢が無くて怒られる (空の配列)', async () => {
+ const res = await api('/notes/create', {
+ poll: {
+ choices: [],
+ },
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('投票の選択肢が1つで怒られる', async () => {
+ const res = await api('/notes/create', {
+ poll: {
+ choices: ['Strawberry Pasta'],
+ },
+ }, alice);
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('投票できる', async () => {
+ const { body } = await api('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako'],
+ },
+ }, alice);
+
+ const res = await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 1,
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ });
+
+ test('複数投票できない', async () => {
+ const { body } = await api('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako'],
+ },
+ }, alice);
+
+ await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 0,
+ }, alice);
+
+ const res = await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 2,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+
+ test('許可されている場合は複数投票できる', async () => {
+ const { body } = await api('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako'],
+ multiple: true,
+ },
+ }, alice);
+
+ await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 0,
+ }, alice);
+
+ await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 1,
+ }, alice);
+
+ const res = await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 2,
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ });
+
+ test('締め切られている場合は投票できない', async () => {
+ const { body } = await api('/notes/create', {
+ text: 'test',
+ poll: {
+ choices: ['sakura', 'izumi', 'ako'],
+ expiredAfter: 1,
+ },
+ }, alice);
+
+ await new Promise(x => setTimeout(x, 2));
+
+ const res = await api('/notes/polls/vote', {
+ noteId: body.createdNote.id,
+ choice: 1,
+ }, alice);
+
+ assert.strictEqual(res.status, 400);
+ });
+ });
+
+ describe('notes/delete', () => {
+ test('delete a reply', async () => {
+ const mainNoteRes = await api('notes/create', {
+ text: 'main post',
+ }, alice);
+ const replyOneRes = await api('notes/create', {
+ text: 'reply one',
+ replyId: mainNoteRes.body.createdNote.id,
+ }, alice);
+ const replyTwoRes = await api('notes/create', {
+ text: 'reply two',
+ replyId: mainNoteRes.body.createdNote.id,
+ }, alice);
+
+ const deleteOneRes = await api('notes/delete', {
+ noteId: replyOneRes.body.createdNote.id,
+ }, alice);
+
+ assert.strictEqual(deleteOneRes.status, 204);
+ let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
+ assert.strictEqual(mainNote.repliesCount, 1);
+
+ const deleteTwoRes = await api('notes/delete', {
+ noteId: replyTwoRes.body.createdNote.id,
+ }, alice);
+
+ assert.strictEqual(deleteTwoRes.status, 204);
+ mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
+ assert.strictEqual(mainNote.repliesCount, 0);
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts
new file mode 100644
index 0000000000..23c431f2e7
--- /dev/null
+++ b/packages/backend/test/e2e/streaming.ts
@@ -0,0 +1,547 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { Following } from '@/models/entities/Following.js';
+import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Streaming', () => {
+ let p: INestApplicationContext;
+ let Followings: any;
+
+ const follow = async (follower: any, followee: any) => {
+ await Followings.save({
+ id: 'a',
+ createdAt: new Date(),
+ followerId: follower.id,
+ followeeId: followee.id,
+ followerHost: follower.host,
+ followerInbox: null,
+ followerSharedInbox: null,
+ followeeHost: followee.host,
+ followeeInbox: null,
+ followeeSharedInbox: null,
+ });
+ };
+
+ describe('Streaming', () => {
+ // Local users
+ let ayano: any;
+ let kyoko: any;
+ let chitose: any;
+
+ // Remote users
+ let akari: any;
+ let chinatsu: any;
+
+ let kyokoNote: any;
+ let list: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ const connection = await initTestDb(true);
+ Followings = connection.getRepository(Following);
+
+ ayano = await signup({ username: 'ayano' });
+ kyoko = await signup({ username: 'kyoko' });
+ chitose = await signup({ username: 'chitose' });
+
+ akari = await signup({ username: 'akari', host: 'example.com' });
+ chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
+
+ kyokoNote = await post(kyoko, { text: 'foo' });
+
+ // Follow: ayano => kyoko
+ await api('following/create', { userId: kyoko.id }, ayano);
+
+ // Follow: ayano => akari
+ await follow(ayano, akari);
+
+ // List: chitose => ayano, kyoko
+ list = await api('users/lists/create', {
+ name: 'my list',
+ }, chitose).then(x => x.body);
+
+ await api('users/lists/push', {
+ listId: list.id,
+ userId: ayano.id,
+ }, chitose);
+
+ await api('users/lists/push', {
+ listId: list.id,
+ userId: kyoko.id,
+ }, chitose);
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ describe('Events', () => {
+ test('mention event', async () => {
+ const fired = await waitFire(
+ kyoko, 'main', // kyoko:main
+ () => post(ayano, { text: 'foo @kyoko bar' }), // ayano mention => kyoko
+ msg => msg.type === 'mention' && msg.body.userId === ayano.id, // wait ayano
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('renote event', async () => {
+ const fired = await waitFire(
+ kyoko, 'main', // kyoko:main
+ () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
+ msg => msg.type === 'renote' && msg.body.renoteId === kyokoNote.id, // wait renote
+ );
+
+ assert.strictEqual(fired, true);
+ });
+ });
+
+ describe('Home Timeline', () => {
+ test('自分の投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'homeTimeline', // ayano:Home
+ () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
+ msg => msg.type === 'note' && msg.body.text === 'foo',
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしているユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'homeTimeline', // ayano:home
+ () => api('notes/create', { text: 'foo' }, kyoko), // kyoko posts
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしていないユーザーの投稿は流れない', async () => {
+ const fired = await waitFire(
+ kyoko, 'homeTimeline', // kyoko:home
+ () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
+ msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'homeTimeline', // ayano:home
+ () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko dm => ayano
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'homeTimeline', // ayano:home
+ () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, kyoko), // kyoko dm => chitose
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, false);
+ });
+ }); // Home
+
+ describe('Local Timeline', () => {
+ test('自分の投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
+ msg => msg.type === 'note' && msg.body.text === 'foo',
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしていないローカルユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo' }, chitose), // chitose posts
+ msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('リモートユーザーの投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
+ msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo' }, akari), // akari posts
+ msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('ホーム指定の投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'localTimeline', // ayano:Local
+ () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
+ msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose
+ );
+
+ assert.strictEqual(fired, false);
+ });
+ });
+
+ describe('Hybrid Timeline', () => {
+ test('自分の投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo' }, ayano), // ayano posts
+ msg => msg.type === 'note' && msg.body.text === 'foo',
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしていないローカルユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo' }, chitose), // chitose posts
+ msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしているリモートユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo' }, akari), // akari posts
+ msg => msg.type === 'note' && msg.body.userId === akari.id, // wait akari
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしていないリモートユーザーの投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
+ msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('フォローしているユーザーのダイレクト投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko),
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしているユーザーのホーム投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko),
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo', visibility: 'home' }, chitose),
+ msg => msg.type === 'note' && msg.body.userId === chitose.id,
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'hybridTimeline', // ayano:Hybrid
+ () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose),
+ msg => msg.type === 'note' && msg.body.userId === chitose.id,
+ );
+
+ assert.strictEqual(fired, false);
+ });
+ });
+
+ describe('Global Timeline', () => {
+ test('フォローしていないローカルユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'globalTimeline', // ayano:Global
+ () => api('notes/create', { text: 'foo' }, chitose), // chitose posts
+ msg => msg.type === 'note' && msg.body.userId === chitose.id, // wait chitose
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('フォローしていないリモートユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ ayano, 'globalTimeline', // ayano:Global
+ () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts
+ msg => msg.type === 'note' && msg.body.userId === chinatsu.id, // wait chinatsu
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('ホーム投稿は流れない', async () => {
+ const fired = await waitFire(
+ ayano, 'globalTimeline', // ayano:Global
+ () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko posts
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
+ );
+
+ assert.strictEqual(fired, false);
+ });
+ });
+
+ describe('UserList Timeline', () => {
+ test('リストに入れているユーザーの投稿が流れる', async () => {
+ const fired = await waitFire(
+ chitose, 'userList',
+ () => api('notes/create', { text: 'foo' }, ayano),
+ msg => msg.type === 'note' && msg.body.userId === ayano.id,
+ { listId: list.id },
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ test('リストに入れていないユーザーの投稿は流れない', async () => {
+ const fired = await waitFire(
+ chitose, 'userList',
+ () => api('notes/create', { text: 'foo' }, chinatsu),
+ msg => msg.type === 'note' && msg.body.userId === chinatsu.id,
+ { listId: list.id },
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ // #4471
+ test('リストに入れているユーザーのダイレクト投稿が流れる', async () => {
+ const fired = await waitFire(
+ chitose, 'userList',
+ () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [chitose.id] }, ayano),
+ msg => msg.type === 'note' && msg.body.userId === ayano.id,
+ { listId: list.id },
+ );
+
+ assert.strictEqual(fired, true);
+ });
+
+ // #4335
+ test('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', async () => {
+ const fired = await waitFire(
+ chitose, 'userList',
+ () => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
+ msg => msg.type === 'note' && msg.body.userId === kyoko.id,
+ { listId: list.id },
+ );
+
+ assert.strictEqual(fired, false);
+ });
+ });
+
+ describe('Hashtag Timeline', () => {
+ test('指定したハッシュタグの投稿が流れる', () => new Promise<void>(async done => {
+ const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+ if (type === 'note') {
+ assert.deepStrictEqual(body.text, '#foo');
+ ws.close();
+ done();
+ }
+ }, {
+ q: [
+ ['foo'],
+ ],
+ });
+
+ post(chitose, {
+ text: '#foo',
+ });
+ }));
+
+ // XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
+
+ // test('指定したハッシュタグの投稿が流れる (AND)', () => new Promise<void>(async done => {
+ // let fooCount = 0;
+ // let barCount = 0;
+ // let fooBarCount = 0;
+
+ // const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+ // if (type === 'note') {
+ // if (body.text === '#foo') fooCount++;
+ // if (body.text === '#bar') barCount++;
+ // if (body.text === '#foo #bar') fooBarCount++;
+ // }
+ // }, {
+ // q: [
+ // ['foo', 'bar'],
+ // ],
+ // });
+
+ // post(chitose, {
+ // text: '#foo',
+ // });
+
+ // post(chitose, {
+ // text: '#bar',
+ // });
+
+ // post(chitose, {
+ // text: '#foo #bar',
+ // });
+
+ // setTimeout(() => {
+ // assert.strictEqual(fooCount, 0);
+ // assert.strictEqual(barCount, 0);
+ // assert.strictEqual(fooBarCount, 1);
+ // ws.close();
+ // done();
+ // }, 3000);
+ // }));
+
+ test('指定したハッシュタグの投稿が流れる (OR)', () => new Promise<void>(async done => {
+ let fooCount = 0;
+ let barCount = 0;
+ let fooBarCount = 0;
+ let piyoCount = 0;
+
+ const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+ if (type === 'note') {
+ if (body.text === '#foo') fooCount++;
+ if (body.text === '#bar') barCount++;
+ if (body.text === '#foo #bar') fooBarCount++;
+ if (body.text === '#piyo') piyoCount++;
+ }
+ }, {
+ q: [
+ ['foo'],
+ ['bar'],
+ ],
+ });
+
+ post(chitose, {
+ text: '#foo',
+ });
+
+ post(chitose, {
+ text: '#bar',
+ });
+
+ post(chitose, {
+ text: '#foo #bar',
+ });
+
+ post(chitose, {
+ text: '#piyo',
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fooCount, 1);
+ assert.strictEqual(barCount, 1);
+ assert.strictEqual(fooBarCount, 1);
+ assert.strictEqual(piyoCount, 0);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+
+ test('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise<void>(async done => {
+ let fooCount = 0;
+ let barCount = 0;
+ let fooBarCount = 0;
+ let piyoCount = 0;
+ let waaaCount = 0;
+
+ const ws = await connectStream(chitose, 'hashtag', ({ type, body }) => {
+ if (type === 'note') {
+ if (body.text === '#foo') fooCount++;
+ if (body.text === '#bar') barCount++;
+ if (body.text === '#foo #bar') fooBarCount++;
+ if (body.text === '#piyo') piyoCount++;
+ if (body.text === '#waaa') waaaCount++;
+ }
+ }, {
+ q: [
+ ['foo', 'bar'],
+ ['piyo'],
+ ],
+ });
+
+ post(chitose, {
+ text: '#foo',
+ });
+
+ post(chitose, {
+ text: '#bar',
+ });
+
+ post(chitose, {
+ text: '#foo #bar',
+ });
+
+ post(chitose, {
+ text: '#piyo',
+ });
+
+ post(chitose, {
+ text: '#waaa',
+ });
+
+ setTimeout(() => {
+ assert.strictEqual(fooCount, 0);
+ assert.strictEqual(barCount, 0);
+ assert.strictEqual(fooBarCount, 1);
+ assert.strictEqual(piyoCount, 1);
+ assert.strictEqual(waaaCount, 0);
+ ws.close();
+ done();
+ }, 3000);
+ }));
+ });
+ });
+});
diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts
new file mode 100644
index 0000000000..792436d88f
--- /dev/null
+++ b/packages/backend/test/e2e/thread-mute.ts
@@ -0,0 +1,103 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, post, connectStream, startServer } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Note thread mute', () => {
+ let p: INestApplicationContext;
+
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
+ const bobNote = await post(bob, { text: '@alice @carol root note' });
+ const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child 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 carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
+
+ const res = await api('/notes/mentions', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false);
+ assert.strictEqual(res.body.some((note: any) => 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' });
+
+ await api('/notes/thread-muting/create', { noteId: bobNote.id }, alice);
+
+ const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' });
+ const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' });
+
+ const res = await api('/i/notifications', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false);
+ assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false);
+
+ // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
+ });
+});
diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts
new file mode 100644
index 0000000000..690cba1746
--- /dev/null
+++ b/packages/backend/test/e2e/user-notes.ts
@@ -0,0 +1,61 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, post, uploadUrl, startServer } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('users/notes', () => {
+ let p: INestApplicationContext;
+
+ let alice: any;
+ let jpgNote: any;
+ let pngNote: any;
+ let jpgPngNote: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
+ const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
+ jpgNote = await post(alice, {
+ fileIds: [jpg.id],
+ });
+ pngNote = await post(alice, {
+ fileIds: [png.id],
+ });
+ jpgPngNote = await post(alice, {
+ fileIds: [jpg.id, png.id],
+ });
+ }, 1000 * 60 * 2);
+
+ afterAll(async() => {
+ await p.close();
+ });
+
+ test('ファイルタイプ指定 (jpg)', async () => {
+ const res = await api('/users/notes', {
+ userId: alice.id,
+ fileType: ['image/jpeg'],
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 2);
+ assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
+ });
+
+ test('ファイルタイプ指定 (jpg or png)', async () => {
+ const res = await api('/users/notes', {
+ userId: alice.id,
+ fileType: ['image/jpeg', 'image/png'],
+ }, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.length, 3);
+ assert.strictEqual(res.body.some((note: any) => note.id === jpgNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === pngNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === jpgPngNote.id), true);
+ });
+});