diff options
| author | Kagami Sascha Rosylight <saschanaz@outlook.com> | 2023-03-03 03:13:12 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-03 11:13:12 +0900 |
| commit | 61215e50ff9e4c84787c8d99c75fd36dafbd8815 (patch) | |
| tree | 36419e8a3ec97afa0a3a0011d523d80addf8e724 /packages/backend/test/e2e | |
| parent | fix(server): チャンネルでミュートが正しく機能していない... (diff) | |
| download | misskey-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.ts | 477 | ||||
| -rw-r--r-- | packages/backend/test/e2e/api.ts | 83 | ||||
| -rw-r--r-- | packages/backend/test/e2e/block.ts | 85 | ||||
| -rw-r--r-- | packages/backend/test/e2e/endpoints.ts | 797 | ||||
| -rw-r--r-- | packages/backend/test/e2e/fetch-resource.ts | 193 | ||||
| -rw-r--r-- | packages/backend/test/e2e/ff-visibility.ts | 165 | ||||
| -rw-r--r-- | packages/backend/test/e2e/mute.ts | 123 | ||||
| -rw-r--r-- | packages/backend/test/e2e/note.ts | 370 | ||||
| -rw-r--r-- | packages/backend/test/e2e/streaming.ts | 547 | ||||
| -rw-r--r-- | packages/backend/test/e2e/thread-mute.ts | 103 | ||||
| -rw-r--r-- | packages/backend/test/e2e/user-notes.ts | 61 |
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); + }); +}); |