From 7bb8c71543a7a41aeab6dad4edbb4088f7ae5126 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 25 Jun 2023 01:34:18 +0200 Subject: chore(backend, misskey-js): add type for signup (#11043) * chore(backend, misskey-js): add type for signup * rerun --- packages/backend/test/e2e/2fa.ts | 45 +++--- packages/backend/test/e2e/antennas.ts | 2 +- packages/backend/test/e2e/api-visibility.ts | 11 +- packages/backend/test/e2e/api.ts | 7 +- packages/backend/test/e2e/block.ts | 7 +- packages/backend/test/e2e/clips.ts | 130 ++++++++--------- packages/backend/test/e2e/endpoints.ts | 11 +- packages/backend/test/e2e/fetch-resource.ts | 209 ++++++++++++++-------------- packages/backend/test/e2e/ff-visibility.ts | 5 +- packages/backend/test/e2e/move.ts | 13 +- packages/backend/test/e2e/mute.ts | 7 +- packages/backend/test/e2e/note.ts | 7 +- packages/backend/test/e2e/renote-mute.ts | 7 +- packages/backend/test/e2e/streaming.ts | 11 +- packages/backend/test/e2e/thread-mute.ts | 7 +- packages/backend/test/e2e/user-notes.ts | 3 +- packages/backend/test/e2e/users.ts | 66 ++++----- 17 files changed, 281 insertions(+), 267 deletions(-) (limited to 'packages/backend/test/e2e') diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 5da997f28b..04be97ad9d 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -7,10 +7,11 @@ import * as OTPAuth from 'otpauth'; import { loadConfig } from '../../src/config.js'; import { signup, api, post, react, startServer, waitFire } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('2要素認証', () => { let app: INestApplicationContext; - let alice: unknown; + let alice: misskey.entities.MeSignup; const config = loadConfig(); const password = 'test'; @@ -68,7 +69,7 @@ describe('2要素認証', () => { ])); // AuthenticatorAssertionResponse.authenticatorData - // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData + // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const credentialIdLength = Buffer.allocUnsafe(2); credentialIdLength.writeUInt16BE(param.credentialId.length); const authData = Buffer.concat([ @@ -80,7 +81,7 @@ describe('2要素認証', () => { param.credentialId, credentialPublicKey, ]); - + return { attestationObject: cbor.encode({ fmt: 'none', @@ -98,7 +99,7 @@ describe('2要素認証', () => { name: param.keyName, }; }; - + const signinParam = (): { username: string, password: string, @@ -130,7 +131,7 @@ describe('2要素認証', () => { 'hcaptcha-response'?: string | null, } => { // AuthenticatorAssertionResponse.authenticatorData - // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData + // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData const authenticatorData = Buffer.concat([ rpIdHash(), Buffer.from([0x05]), // flags(1) @@ -146,7 +147,7 @@ describe('2要素認証', () => { .update(clientDataJSONBuffer) .digest(); const privateKey = crypto.createPrivateKey(pemToSign); - const signature = crypto.createSign('SHA256') + const signature = crypto.createSign('SHA256') .update(Buffer.concat([authenticatorData, hashedclientDataJSON])) .sign(privateKey); return { @@ -186,14 +187,14 @@ describe('2要素認証', () => { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 204); - + const usersShowResponse = await api('/users/show', { username, }, alice); assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); - - const signinResponse = await api('/signin', { + + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); @@ -211,7 +212,7 @@ describe('2要素認証', () => { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 204); - + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); @@ -230,7 +231,7 @@ describe('2要素認証', () => { assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex')); assert.strictEqual(keyDoneResponse.body.name, keyName); - + const usersShowResponse = await api('/users/show', { username, }); @@ -267,7 +268,7 @@ describe('2要素認証', () => { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 204); - + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); @@ -282,7 +283,7 @@ describe('2要素認証', () => { credentialId, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - + const passwordLessResponse = await api('/i/2fa/password-less', { value: true, }, alice); @@ -301,7 +302,7 @@ describe('2要素認証', () => { assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.body.i, undefined); - const signinResponse2 = await api('/signin', { + const signinResponse2 = await api('/signin', { ...signinWithSecurityKeyParam({ keyName, challengeId: signinResponse.body.challengeId, @@ -324,7 +325,7 @@ describe('2要素認証', () => { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 204); - + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); @@ -339,14 +340,14 @@ describe('2要素認証', () => { credentialId, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - + const renamedKey = 'other-key'; const updateKeyResponse = await api('/i/2fa/update-key', { name: renamedKey, credentialId: credentialId.toString('hex'), }, alice); assert.strictEqual(updateKeyResponse.status, 200); - + const iResponse = await api('/i', { }, alice); assert.strictEqual(iResponse.status, 200); @@ -366,7 +367,7 @@ describe('2要素認証', () => { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 204); - + const registerKeyResponse = await api('/i/2fa/register-key', { password, }, alice); @@ -381,7 +382,7 @@ describe('2要素認証', () => { credentialId, }), alice); assert.strictEqual(keyDoneResponse.status, 200); - + // テストの実行順によっては複数残ってるので全部消す const iResponse = await api('/i', { }, alice); @@ -400,14 +401,14 @@ describe('2要素認証', () => { assert.strictEqual(usersShowResponse.status, 200); assert.strictEqual(usersShowResponse.body.securityKeys, false); - const signinResponse = await api('/signin', { + const signinResponse = await api('/signin', { ...signinParam(), token: otpToken(registerResponse.body.secret), }); assert.strictEqual(signinResponse.status, 200); assert.notEqual(signinResponse.body.i, undefined); }); - + test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => { const registerResponse = await api('/i/2fa/register', { password, @@ -418,7 +419,7 @@ describe('2要素認証', () => { token: otpToken(registerResponse.body.secret), }, alice); assert.strictEqual(doneResponse.status, 204); - + const usersShowResponse = await api('/users/show', { username, }); diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index dd3b09f85a..cb526669f5 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -32,7 +32,7 @@ describe('アンテナ', () => { // - srcのenumにgroupが残っている // - userGroupIdが残っている, isActiveがない type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; - type User = misskey.entities.MeDetailed & { token: string }; + type User = misskey.entities.MeSignup; type Note = misskey.entities.Note; // アンテナを作成できる最小のパラメタ diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 3af0d35182..f781559d50 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -3,6 +3,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('API visibility', () => { let app: INestApplicationContext; @@ -18,15 +19,15 @@ describe('API visibility', () => { describe('Note visibility', () => { //#region vars /** ヒロイン */ - let alice: any; + let alice: misskey.entities.MeSignup; /** フォロワー */ - let follower: any; + let follower: misskey.entities.MeSignup; /** 非フォロワー */ - let other: any; + let other: misskey.entities.MeSignup; /** 非フォロワーでもリプライやメンションをされた人 */ - let target: any; + let target: misskey.entities.MeSignup; /** specified mentionでmentionを飛ばされる人 */ - let target2: any; + let target2: misskey.entities.MeSignup; /** public-post */ let pub: any; diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index a46f336a70..194ded7e8b 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -3,12 +3,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('API', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 57a46ab38a..8357884092 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -3,14 +3,15 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, post, startServer } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Block', () => { let app: INestApplicationContext; // alice blocks bob - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index f35aae9dc6..175f2cac97 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -13,12 +13,12 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; -import { - signup, - post, - startServer, +import { + signup, + post, + startServer, api, - successfulApiCall, + successfulApiCall, failedApiCall, ApiRequest, hiddenNote, @@ -82,14 +82,14 @@ describe('クリップ', () => { const update = async (parameters: Partial, request: Partial = {}): Promise => { const clip = await successfulApiCall({ endpoint: '/clips/update', - parameters: { + parameters: { name: 'updated', ...parameters, }, user: alice, ...request, }); - + // 入力が結果として入っていること。clipIdはidになるので消しておく delete (parameters as { clipId?: string }).clipId; assert.deepStrictEqual(clip, { @@ -98,7 +98,7 @@ describe('クリップ', () => { }); return clip; }; - + type DeleteParam = JTDDataType; const deleteClip = async (parameters: DeleteParam, request: Partial = {}): Promise => { return await successfulApiCall({ @@ -129,7 +129,7 @@ describe('クリップ', () => { ...request, }); }; - + const usersClips = async (request: Partial): Promise => { return await successfulApiCall({ endpoint: '/users/clips', @@ -145,14 +145,14 @@ describe('クリップ', () => { bob = await signup({ username: 'bob' }); // FIXME: misskey-jsのNoteはoutdatedなので直接変換できない - aliceNote = await post(alice, { text: 'test' }) as any; - aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; - aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; - aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; - bobNote = await post(bob, { text: 'test' }) as any; - bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; - bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; - bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; + aliceNote = await post(alice, { text: 'test' }) as any; + aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; + aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; + aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; + bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; + bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; + bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; }, 1000 * 60 * 2); afterAll(async () => { @@ -172,7 +172,7 @@ describe('クリップ', () => { test('の作成ができる', async () => { const res = await create(); // ISO 8601で日付が返ってくること - assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); assert.strictEqual(res.lastClippedAt, null); assert.strictEqual(res.name, 'test'); assert.strictEqual(res.description, null); @@ -217,7 +217,7 @@ describe('クリップ', () => { ]; test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ endpoint: '/clips/create', - parameters: { + parameters: { ...defaultCreate(), ...parameters, }, @@ -229,7 +229,7 @@ describe('クリップ', () => { })); test('の更新ができる', async () => { - const res = await update({ + const res = await update({ clipId: (await create()).id, name: 'updated', description: 'new description', @@ -237,7 +237,7 @@ describe('クリップ', () => { }); // ISO 8601で日付が返ってくること - assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); assert.strictEqual(res.lastClippedAt, null); assert.strictEqual(res.name, 'updated'); assert.strictEqual(res.description, 'new description'); @@ -251,7 +251,7 @@ describe('クリップ', () => { name: 'updated', ...parameters, })); - + test.each([ { label: 'clipIdがnull', parameters: { clipId: null } }, { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { @@ -265,7 +265,7 @@ describe('クリップ', () => { ...createClipDenyPattern as any, ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/update', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, name: 'updated', ...parameters, @@ -279,7 +279,7 @@ describe('クリップ', () => { })); test('の削除ができる', async () => { - await deleteClip({ + await deleteClip({ clipId: (await create()).id, }); assert.deepStrictEqual(await list({}), []); @@ -297,7 +297,7 @@ describe('クリップ', () => { } }, ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/delete', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, @@ -329,14 +329,14 @@ describe('クリップ', () => { }); test.each([ - { label: 'clipId未指定', parameters: { clipId: undefined } }, - { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { code: 'NO_SUCH_CLIP', id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', } }, ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ endpoint: '/clips/show', - parameters: { + parameters: { ...parameters, }, user: alice, @@ -361,14 +361,14 @@ describe('クリップ', () => { // 返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), clips.sort(compareBy(s => s.id)), ); }); test('の一覧が取得できる(空)', async () => { const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, }, }); @@ -381,14 +381,14 @@ describe('クリップ', () => { ])('の一覧が$label取得できる', async () => { const clips = await createMany({ isPublic: true }); const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, }, }); // 返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), clips.sort(compareBy(s => s.id))); // 認証状態で見たときだけisFavoritedが入っている @@ -421,7 +421,7 @@ describe('クリップ', () => { await create({ isPublic: false }); const aliceClip = await create({ isPublic: true }); const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, limit: 2, }, @@ -433,7 +433,7 @@ describe('クリップ', () => { const clips = await createMany({ isPublic: true }, 7); clips.sort(compareBy(s => s.id)); const res = await usersClips({ - parameters: { + parameters: { userId: alice.id, sinceId: clips[1].id, untilId: clips[5].id, @@ -443,7 +443,7 @@ describe('クリップ', () => { // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 assert.deepStrictEqual( - res.sort(compareBy(s => s.id)), + res.sort(compareBy(s => s.id)), [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); }); @@ -454,7 +454,7 @@ describe('クリップ', () => { { label: 'limit最大+1', parameters: { limit: 101 } }, ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ endpoint: '/users/clips', - parameters: { + parameters: { userId: alice.id, ...parameters, }, @@ -520,7 +520,7 @@ describe('クリップ', () => { ...request, }); }; - + beforeEach(async () => { aliceClip = await create(); }); @@ -544,7 +544,7 @@ describe('クリップ', () => { assert.strictEqual(clip2.favoritedCount, 1); assert.strictEqual(clip2.isFavorited, false); }); - + test('は1つのクリップに対して複数人が設定できる。', async () => { const publicClip = await create({ isPublic: true }); await favorite({ clipId: publicClip.id }, { user: bob }); @@ -552,7 +552,7 @@ describe('クリップ', () => { const clip = await show({ clipId: publicClip.id }, { user: bob }); assert.strictEqual(clip.favoritedCount, 2); assert.strictEqual(clip.isFavorited, true); - + const clip2 = await show({ clipId: publicClip.id }); assert.strictEqual(clip2.favoritedCount, 2); assert.strictEqual(clip2.isFavorited, true); @@ -581,7 +581,7 @@ describe('クリップ', () => { await favorite({ clipId: aliceClip.id }); await failedApiCall({ endpoint: '/clips/favorite', - parameters: { + parameters: { clipId: aliceClip.id, }, user: alice, @@ -604,7 +604,7 @@ describe('クリップ', () => { } }, ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/favorite', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, @@ -615,7 +615,7 @@ describe('クリップ', () => { id: '3d81ceae-475f-4600-b2a8-2bc116157532', ...assertion, })); - + test('を設定解除できる。', async () => { await favorite({ clipId: aliceClip.id }); await unfavorite({ clipId: aliceClip.id }); @@ -641,7 +641,7 @@ describe('クリップ', () => { } }, ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/unfavorite', - parameters: { + parameters: { clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, ...parameters, }, @@ -652,7 +652,7 @@ describe('クリップ', () => { id: '3d81ceae-475f-4600-b2a8-2bc116157532', ...assertion, })); - + test('を取得できる。', async () => { await favorite({ clipId: aliceClip.id }); const favorited = await myFavorites(); @@ -717,7 +717,7 @@ describe('クリップ', () => { const res = await show({ clipId: aliceClip.id }); assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]); - + // 他人の非公開ノートも突っ込める await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id }); await addNote({ clipId: aliceClip.id, noteId: bobFollowersNote.id }); @@ -728,7 +728,7 @@ describe('クリップ', () => { await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); await failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, }, @@ -747,10 +747,10 @@ describe('クリップ', () => { text: `test ${i}`, }) as unknown)) as Note[]; await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); - + await failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, }, @@ -764,7 +764,7 @@ describe('クリップ', () => { test('は他人のクリップへ追加できない。', async () => await failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, }, @@ -776,9 +776,9 @@ describe('クリップ', () => { })); test.each([ - { label: 'clipId未指定', parameters: { clipId: undefined } }, - { label: 'noteId未指定', parameters: { noteId: undefined } }, - { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { code: 'NO_SUCH_CLIP', id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', } }, @@ -792,7 +792,7 @@ describe('クリップ', () => { } }, ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ endpoint: '/clips/add-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, ...parameters, @@ -810,11 +810,11 @@ describe('クリップ', () => { await removeNote({ clipId: aliceClip.id, noteId: aliceNote.id }); assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), []); }); - + test.each([ - { label: 'clipId未指定', parameters: { clipId: undefined } }, - { label: 'noteId未指定', parameters: { noteId: undefined } }, - { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { code: 'NO_SUCH_CLIP', id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる } }, @@ -828,7 +828,7 @@ describe('クリップ', () => { } }, ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ endpoint: '/clips/remove-note', - parameters: { + parameters: { clipId: aliceClip.id, noteId: aliceNote.id, ...parameters, @@ -848,12 +848,12 @@ describe('クリップ', () => { } const res = await notes({ clipId: aliceClip.id }); - + // 自分のノートは非公開でも入れられるし、見える // 他人の非公開ノートは入れられるけど、除外される const expects = [ aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, - bobNote, bobHomeNote, + bobNote, bobHomeNote, ]; assert.deepStrictEqual( res.sort(compareBy(s => s.id)), @@ -867,7 +867,7 @@ describe('クリップ', () => { await addNote({ clipId: aliceClip.id, noteId: note.id }); } - const res = await notes({ + const res = await notes({ clipId: aliceClip.id, sinceId: noteList[2].id, limit: 3, @@ -892,7 +892,7 @@ describe('クリップ', () => { sinceId: noteList[1].id, untilId: noteList[4].id, }); - + // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 const expects = [noteList[2], noteList[3]]; assert.deepStrictEqual( @@ -918,7 +918,7 @@ describe('クリップ', () => { const res = await notes({ clipId: publicClip.id }, { user: undefined }); const expects = [ - aliceNote, aliceHomeNote, + aliceNote, aliceHomeNote, // 認証なしだと非公開ノートは結果には含むけどhideされる。 hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), ]; @@ -926,7 +926,7 @@ describe('クリップ', () => { res.sort(compareBy(s => s.id)), expects.sort(compareBy(s => s.id))); }); - + test.todo('ブロック、ミュートされたユーザーからの設定&取得etc.'); test.each([ @@ -947,7 +947,7 @@ describe('クリップ', () => { } }, ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ endpoint: '/clips/notes', - parameters: { + parameters: { clipId: aliceClip.id, ...parameters, }, diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index f885209b7f..a1e89d4833 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -4,17 +4,18 @@ 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 { User } from '@/models/index.js'; import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; -import { User } from '@/models/index.js'; +import type * as misskey from 'misskey-js'; describe('Endpoints', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; - let carol: any; - let dave: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 78ca8b43ba..115945dd3d 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -4,6 +4,7 @@ import * as assert from 'assert'; import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; // Request Accept const ONLY_AP = 'application/activity+json'; @@ -19,7 +20,7 @@ const JSON_UTF8 = 'application/json; charset=utf-8'; describe('Webリソース', () => { let app: INestApplicationContext; - let alice: any; + let alice: misskey.entities.MeSignup; let aliceUploadedFile: any; let alicesPost: any; let alicePage: any; @@ -28,8 +29,8 @@ describe('Webリソース', () => { let aliceGalleryPost: any; let aliceChannel: any; - type Request = { - path: string, + type Request = { + path: string, accept?: string, cookie?: string, }; @@ -46,7 +47,7 @@ describe('Webリソース', () => { const notOk = async (param: Request & { status?: number, code?: string, - }): Promise => { + }): Promise => { const { path, accept, cookie, status, code } = param; const res = await simpleGet(path, accept, cookie); assert.notStrictEqual(res.status, 200); @@ -58,8 +59,8 @@ describe('Webリソース', () => { } return res; }; - - const notFound = async (param: Request): Promise => { + + const notFound = async (param: Request): Promise => { return await notOk({ ...param, status: 404, @@ -94,23 +95,23 @@ describe('Webリソース', () => { { path: '/', type: HTML }, { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" // fastify-static gives charset=UTF-8 instead of utf-8 and that's okay - { path: '/api-doc', type: 'text/html; charset=UTF-8' }, - { path: '/api.json', type: JSON_UTF8 }, - { path: '/api-console', type: HTML }, - { path: '/_info_card_', type: HTML }, - { path: '/bios', type: HTML }, - { path: '/cli', type: HTML }, - { path: '/flush', type: HTML }, + { path: '/api-doc', type: 'text/html; charset=UTF-8' }, + { path: '/api.json', type: JSON_UTF8 }, + { path: '/api-console', type: HTML }, + { path: '/_info_card_', type: HTML }, + { path: '/bios', type: HTML }, + { path: '/cli', type: HTML }, + { path: '/flush', type: HTML }, { path: '/robots.txt', type: 'text/plain; charset=UTF-8' }, - { path: '/favicon.ico', type: 'image/vnd.microsoft.icon' }, + { path: '/favicon.ico', type: 'image/vnd.microsoft.icon' }, { path: '/opensearch.xml', type: 'application/opensearchdescription+xml' }, - { path: '/apple-touch-icon.png', type: 'image/png' }, - { path: '/twemoji/2764.svg', type: 'image/svg+xml' }, - { path: '/twemoji/2764-fe0f-200d-1f525.svg', type: 'image/svg+xml' }, - { path: '/twemoji-badge/2764.png', type: 'image/png' }, + { path: '/apple-touch-icon.png', type: 'image/png' }, + { path: '/twemoji/2764.svg', type: 'image/svg+xml' }, + { path: '/twemoji/2764-fe0f-200d-1f525.svg', type: 'image/svg+xml' }, + { path: '/twemoji-badge/2764.png', type: 'image/png' }, { path: '/twemoji-badge/2764-fe0f-200d-1f525.png', type: 'image/png' }, - { path: '/fluent-emoji/2764.png', type: 'image/png' }, - { path: '/fluent-emoji/2764-fe0f-200d-1f525.png', type: 'image/png' }, + { path: '/fluent-emoji/2764.png', type: 'image/png' }, + { path: '/fluent-emoji/2764-fe0f-200d-1f525.png', type: 'image/png' }, ])('$path', (p) => { test('がGETできる。', async () => await ok({ ...p })); @@ -120,58 +121,58 @@ describe('Webリソース', () => { }); describe.each([ - { path: '/twemoji/2764.png' }, - { path: '/twemoji/2764-fe0f-200d-1f525.png' }, - { path: '/twemoji-badge/2764.svg' }, + { path: '/twemoji/2764.png' }, + { path: '/twemoji/2764-fe0f-200d-1f525.png' }, + { path: '/twemoji-badge/2764.svg' }, { path: '/twemoji-badge/2764-fe0f-200d-1f525.svg' }, - { path: '/fluent-emoji/2764.svg' }, - { path: '/fluent-emoji/2764-fe0f-200d-1f525.svg' }, + { path: '/fluent-emoji/2764.svg' }, + { path: '/fluent-emoji/2764-fe0f-200d-1f525.svg' }, ])('$path', ({ path }) => { test('はGETできない。', async () => await notFound({ path })); }); describe.each([ - { ext: 'rss', type: 'application/rss+xml; charset=utf-8' }, - { ext: 'atom', type: 'application/atom+xml; charset=utf-8' }, - { ext: 'json', type: 'application/json; charset=utf-8' }, + { ext: 'rss', type: 'application/rss+xml; charset=utf-8' }, + { ext: 'atom', type: 'application/atom+xml; charset=utf-8' }, + { ext: 'json', type: 'application/json; charset=utf-8' }, ])('/@:username.$ext', ({ ext, type }) => { const path = (username: string): string => `/@${username}.${ext}`; - test('がGETできる。', async () => await ok({ + test('がGETできる。', async () => await ok({ path: path(alice.username), type, })); - test('は存在しないユーザーはGETできない。', async () => await notOk({ + test('は存在しないユーザーはGETできない。', async () => await notOk({ path: path('nonexisting'), - status: 404, + status: 404, })); }); describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { - test('はGETできない。', async () => await notOk({ + test('はGETできない。', async () => await notOk({ path, - status: 404, + status: 404, code: 'UNKNOWN_API_ENDPOINT', })); }); describe.each([{ path: '/queue' }])('$path', ({ path }) => { - test('はadminでなければGETできない。', async () => await notOk({ + test('はadminでなければGETできない。', async () => await notOk({ path, status: 500, // FIXME? 403ではない。 })); - - test('はadminならGETできる。', async () => await ok({ + + test('はadminならGETできる。', async () => await ok({ path, cookie: cookie(alice), - })); + })); }); describe.each([{ path: '/streaming' }])('$path', ({ path }) => { - test('はGETできない。', async () => await notOk({ + test('はGETできない。', async () => await notOk({ path, - status: 503, + status: 503, })); }); @@ -183,21 +184,21 @@ describe('Webリソース', () => { { accept: UNSPECIFIED }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username), - accept, + const res = await ok({ + path: path(alice.username), + accept, type: HTML, }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); - + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 // TODO の検証 }); - test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), type: HTML, })); }); @@ -207,22 +208,22 @@ describe('Webリソース', () => { { accept: PREFER_AP }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はActivityPubとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username), - accept, + const res = await ok({ + path: path(alice.username), + accept, type: AP, }); assert.strictEqual(res.body.type, 'Person'); }); - test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ - path: path('xxxxxxxxxx'), + test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ + path: path('xxxxxxxxxx'), accept, })); }); }); - describe.each([ + describe.each([ // 実際のハンドルはフロントエンド(index.vue)で行われる { sub: 'home' }, { sub: 'notes' }, @@ -236,32 +237,32 @@ describe('Webリソース', () => { const path = (username: string): string => `/@${username}/${sub}`; test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username), + const res = await ok({ + path: path(alice.username), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); }); }); - + describe('/@:user/pages/:page', () => { const path = (username: string, pagename: string): string => `/@${username}/pages/${pagename}`; test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.username, alicePage.name), + const res = await ok({ + path: path(alice.username, alicePage.name), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); assert.strictEqual(metaTag(res, 'misskey:page-id'), alicePage.id); - + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - - test('はGETできる。(存在しないIDでも。)', async () => await ok({ - path: path(alice.username, 'xxxxxxxxxx'), + + test('はGETできる。(存在しないIDでも。)', async () => await ok({ + path: path(alice.username, 'xxxxxxxxxx'), })); }); @@ -278,7 +279,7 @@ describe('Webリソース', () => { assert.strictEqual(res.location, `/@${alice.username}`); }); - test('は存在しないユーザーはGETできない。', async () => await notFound({ + test('は存在しないユーザーはGETできない。', async () => await notFound({ path: path('xxxxxxxx'), })); }); @@ -288,24 +289,24 @@ describe('Webリソース', () => { { accept: PREFER_AP }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はActivityPubとしてGETできる。', async () => { - const res = await ok({ - path: path(alice.id), - accept, + const res = await ok({ + path: path(alice.id), + accept, type: AP, }); assert.strictEqual(res.body.type, 'Person'); }); - test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notOk({ + test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notOk({ path: path('xxxxxxxx'), accept, status: 404, })); }); }); - + describe('/users/inbox', () => { - test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ + test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ path: '/inbox', })); @@ -315,7 +316,7 @@ describe('Webリソース', () => { describe('/users/:id/inbox', () => { const path = (id: string): string => `/users/${id}/inbox`; - test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ + test('がGETできる。(POST専用だけど4xx/5xxにならずHTMLが返ってくる)', async () => await ok({ path: path(alice.id), })); @@ -326,14 +327,14 @@ describe('Webリソース', () => { const path = (id: string): string => `/users/${id}/outbox`; test('がGETできる。', async () => { - const res = await ok({ - path: path(alice.id), + const res = await ok({ + path: path(alice.id), type: AP, }); assert.strictEqual(res.body.type, 'OrderedCollection'); }); }); - + describe('/notes/:id', () => { const path = (noteId: string): string => `/notes/${noteId}`; @@ -342,22 +343,22 @@ describe('Webリソース', () => { { accept: UNSPECIFIED }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はHTMLとしてGETできる。', async () => { - const res = await ok({ - path: path(alicesPost.id), - accept, + const res = await ok({ + path: path(alicesPost.id), + accept, type: HTML, }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); - assert.strictEqual(metaTag(res, 'misskey:note-id'), alicesPost.id); - + assert.strictEqual(metaTag(res, 'misskey:note-id'), alicesPost.id); + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + test('はHTMLとしてGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); @@ -366,48 +367,48 @@ describe('Webリソース', () => { { accept: PREFER_AP }, ])('(Acceptヘッダ: $accept)', ({ accept }) => { test('はActivityPubとしてGETできる。', async () => { - const res = await ok({ - path: path(alicesPost.id), + const res = await ok({ + path: path(alicesPost.id), accept, type: AP, }); assert.strictEqual(res.body.type, 'Note'); }); - test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ - path: path('xxxxxxxxxx'), + test('は存在しないIDのときActivityPubとしてGETできない。', async () => await notFound({ + path: path('xxxxxxxxxx'), accept, })); }); }); - + describe('/play/:id', () => { const path = (playid: string): string => `/play/${playid}`; test('がGETできる。', async () => { - const res = await ok({ - path: path(alicePlay.id), + const res = await ok({ + path: path(alicePlay.id), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); assert.strictEqual(metaTag(res, 'misskey:flash-id'), alicePlay.id); - + // TODO ogタグの検証 // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); - + describe('/clips/:clip', () => { const path = (clip: string): string => `/clips/${clip}`; test('がGETできる。', async () => { - const res = await ok({ - path: path(aliceClip.id), + const res = await ok({ + path: path(aliceClip.id), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); @@ -416,9 +417,9 @@ describe('Webリソース', () => { // TODO ogタグの検証 // TODO profile.noCrawleの検証 }); - - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); @@ -426,8 +427,8 @@ describe('Webリソース', () => { const path = (post: string): string => `/gallery/${post}`; test('がGETできる。', async () => { - const res = await ok({ - path: path(aliceGalleryPost.id), + const res = await ok({ + path: path(aliceGalleryPost.id), }); assert.strictEqual(metaTag(res, 'misskey:user-username'), alice.username); assert.strictEqual(metaTag(res, 'misskey:user-id'), alice.id); @@ -436,26 +437,26 @@ describe('Webリソース', () => { // TODO profile.noCrawleの検証 // TODO twitter:creatorの検証 }); - - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); - + describe('/channels/:channel', () => { const path = (channel: string): string => `/channels/${channel}`; test('はGETできる。', async () => { const res = await ok({ - path: path(aliceChannel.id), + path: path(aliceChannel.id), }); // FIXME: misskey関連のmetaタグの設定がない // TODO ogタグの検証 }); - - test('がGETできる。(存在しないIDでも。)', async () => await ok({ - path: path('xxxxxxxxxx'), + + test('がGETできる。(存在しないIDでも。)', async () => await ok({ + path: path('xxxxxxxxxx'), })); }); }); diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 7b75005a39..9082c77f07 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -3,12 +3,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { signup, api, startServer, simpleGet } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('FF visibility', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 7d6c646090..cd9459fa52 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -7,6 +7,7 @@ import { User, UsersRepository } from '@/models/index.js'; import { jobQueue } from '@/boot/common.js'; import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Account Move', () => { let app: INestApplicationContext; @@ -14,12 +15,12 @@ describe('Account Move', () => { let url: URL; let root: any; - let alice: any; - let bob: any; - let carol: any; - let dave: any; - let eve: any; - let frank: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; + let dave: misskey.entities.MeSignup; + let eve: misskey.entities.MeSignup; + let frank: misskey.entities.MeSignup; let Users: UsersRepository; diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index 25bd532cfb..79e2c90f64 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -3,14 +3,15 @@ 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'; +import type * as misskey from 'misskey-js'; describe('Mute', () => { let app: INestApplicationContext; // alice mutes carol - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index d2eb8f01d7..33da811a26 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -4,13 +4,14 @@ import * as assert from 'assert'; import { Note } from '@/models/entities/Note.js'; import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; describe('Note', () => { let app: INestApplicationContext; let Notes: any; - let alice: any; - let bob: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); @@ -378,7 +379,7 @@ describe('Note', () => { }, }, }, alice); - + assert.strictEqual(res.status, 200); const assign = await api('admin/roles/assign', { diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 0f73b8d09f..72fc599aaf 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -3,14 +3,15 @@ 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'; +import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { let app: INestApplicationContext; // alice mutes carol - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index d1394ef7a8..2cddafed2e 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -4,6 +4,7 @@ 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'; +import type * as misskey from 'misskey-js'; describe('Streaming', () => { let app: INestApplicationContext; @@ -26,13 +27,13 @@ describe('Streaming', () => { describe('Streaming', () => { // Local users - let ayano: any; - let kyoko: any; - let chitose: any; + let ayano: misskey.entities.MeSignup; + let kyoko: misskey.entities.MeSignup; + let chitose: misskey.entities.MeSignup; // Remote users - let akari: any; - let chinatsu: any; + let akari: misskey.entities.MeSignup; + let chinatsu: misskey.entities.MeSignup; let kyokoNote: any; let list: any; diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 2ae2eb67c1..e01ea90fe0 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -3,13 +3,14 @@ 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'; +import type * as misskey from 'misskey-js'; describe('Note thread mute', () => { let app: INestApplicationContext; - let alice: any; - let bob: any; - let carol: any; + let alice: misskey.entities.MeSignup; + let bob: misskey.entities.MeSignup; + let carol: misskey.entities.MeSignup; beforeAll(async () => { app = await startServer(); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index c11099e7b5..3681456c7e 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -3,11 +3,12 @@ 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'; +import type * as misskey from 'misskey-js'; describe('users/notes', () => { let app: INestApplicationContext; - let alice: any; + let alice: misskey.entities.MeSignup; let jpgNote: any; let pngNote: any; let jpgPngNote: any; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 02684c93b8..64efaa57cc 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -4,14 +4,14 @@ import * as assert from 'assert'; import { inspect } from 'node:util'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { - signup, - post, +import { + signup, + post, page, role, - startServer, + startServer, api, - successfulApiCall, + successfulApiCall, failedApiCall, uploadFile, } from '../utils.js'; @@ -36,19 +36,19 @@ describe('ユーザー', () => { badgeRoles: any[], }; - type UserDetailedNotMe = UserLite & + type UserDetailedNotMe = UserLite & misskey.entities.UserDetailed & { roles: any[], }; - type MeDetailed = UserDetailedNotMe & + type MeDetailed = UserDetailedNotMe & misskey.entities.MeDetailed & { achievements: object[], loggedInDays: number, policies: object, }; - - type User = MeDetailed & { token: string }; + + type User = MeDetailed & { token: string }; const show = async (id: string, me = root): Promise => { return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any; @@ -159,7 +159,7 @@ describe('ユーザー', () => { mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, emailNotificationTypes: user.emailNotificationTypes, - achievements: user.achievements, + achievements: user.achievements, loggedInDays: user.loggedInDays, policies: user.policies, ...(security ? { @@ -222,11 +222,11 @@ describe('ユーザー', () => { beforeAll(async () => { root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); - aliceNote = await post(alice, { text: 'test' }) as any; + aliceNote = await post(alice, { text: 'test' }) as any; alicePage = await page(alice); aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body; bob = await signup({ username: 'bob' }); - bobNote = await post(bob, { text: 'test' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; carol = await signup({ username: 'carol' }); dave = await signup({ username: 'dave' }); ellen = await signup({ username: 'ellen' }); @@ -236,10 +236,10 @@ describe('ユーザー', () => { usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { const u = await signup({ username: `replying${i}` }); for (let j = 0; j < 10 - i; j++) { - const p = await post(u, { text: `test${j}` }); + const p = await post(u, { text: `test${j}` }); await post(alice, { text: `@${u.username} test${j}`, replyId: p.id }); } - + return (await acc).concat(u); }, Promise.resolve([] as User[])); @@ -376,7 +376,7 @@ describe('ユーザー', () => { assert.strictEqual(response.securityKeys, false); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); - + // MeDetailedOnly assert.strictEqual(response.avatarId, null); assert.strictEqual(response.bannerId, null); @@ -406,7 +406,7 @@ describe('ユーザー', () => { assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); assert.deepStrictEqual(response.achievements, []); assert.deepStrictEqual(response.loggedInDays, 0); - assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); + assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); assert.notStrictEqual(response.email, undefined); assert.strictEqual(response.emailVerified, false); assert.deepStrictEqual(response.securityKeysList, []); @@ -499,8 +499,8 @@ describe('ユーザー', () => { const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); - const expected = { - ...meDetailed(alice, true), + const expected = { + ...meDetailed(alice, true), avatarId: aliceFile.id, avatarBlurhash: response.avatarBlurhash, avatarUrl: response.avatarUrl, @@ -509,8 +509,8 @@ describe('ユーザー', () => { const parameters2 = { avatarId: null }; const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice }); - const expected2 = { - ...meDetailed(alice, true), + const expected2 = { + ...meDetailed(alice, true), avatarId: null, avatarBlurhash: null, avatarUrl: alice.avatarUrl, // 解除した場合、identiconになる @@ -524,8 +524,8 @@ describe('ユーザー', () => { const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); - const expected = { - ...meDetailed(alice, true), + const expected = { + ...meDetailed(alice, true), bannerId: aliceFile.id, bannerBlurhash: response.bannerBlurhash, bannerUrl: response.bannerUrl, @@ -534,8 +534,8 @@ describe('ユーザー', () => { const parameters2 = { bannerId: null }; const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice }); - const expected2 = { - ...meDetailed(alice, true), + const expected2 = { + ...meDetailed(alice, true), bannerId: null, bannerBlurhash: null, bannerUrl: null, @@ -551,7 +551,7 @@ describe('ユーザー', () => { const response = await successfulApiCall({ endpoint: 'i/pin', parameters, user: alice }); const expected = { ...meDetailed(alice, false), pinnedNoteIds: [aliceNote.id], pinnedNotes: [aliceNote] }; assert.deepStrictEqual(response, expected); - + const response2 = await successfulApiCall({ endpoint: 'i/unpin', parameters, user: alice }); const expected2 = meDetailed(alice, false); assert.deepStrictEqual(response2, expected2); @@ -612,7 +612,7 @@ describe('ユーザー', () => { }); test.todo('をリスト形式で取得することができる(リモート, hostname指定)'); test.todo('をリスト形式で取得することができる(pagenation)'); - + //#endregion //#region ユーザー情報(users/show) @@ -684,9 +684,9 @@ describe('ユーザー', () => { const parameters = { userIds: [bob.id, alice.id, carol.id] }; const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice }); const expected = [ - await successfulApiCall({ endpoint: 'users/show', parameters: { userId: bob.id }, user: alice }), - await successfulApiCall({ endpoint: 'users/show', parameters: { userId: alice.id }, user: alice }), - await successfulApiCall({ endpoint: 'users/show', parameters: { userId: carol.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: bob.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: alice.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: carol.id }, user: alice }), ]; assert.deepStrictEqual(response, expected); }); @@ -701,7 +701,7 @@ describe('ユーザー', () => { // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { const parameters = { userIds: [user().id] }; const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); @@ -734,7 +734,7 @@ describe('ユーザー', () => { { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, - { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { const parameters = { query: user().username, limit: 1 }; const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); @@ -747,7 +747,7 @@ describe('ユーザー', () => { //#endregion //#region ID指定検索(users/search-by-username-and-host) - test.each([ + test.each([ { label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, @@ -786,7 +786,7 @@ describe('ユーザー', () => { test('がよくリプライをするユーザーのリストを取得できる', async () => { const parameters = { userId: alice.id, limit: 5 }; const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice }); - const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({ + const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({ user: await show(s.id, alice), weight: (usersReplying.length - i) / usersReplying.length, }))); -- cgit v1.2.3-freya From ef354e94f20ace67b94faa2859c458a436cdd3f7 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 25 Jun 2023 04:04:33 +0200 Subject: refactor(backend): replace rndstr with secureRndstr (#11044) * refactor(backend): replace rndstr with secureRndstr * Update pnpm-lock.yaml * .js --- packages/backend/package.json | 1 - .../backend/src/misc/generate-native-user-token.ts | 2 +- packages/backend/src/misc/secure-rndstr.ts | 5 ++-- .../backend/src/server/api/SignupApiService.ts | 28 ++++++++++---------- .../src/server/api/endpoints/admin/emoji/add.ts | 1 - .../server/api/endpoints/admin/reset-password.ts | 4 +-- .../backend/src/server/api/endpoints/app/create.ts | 2 +- .../src/server/api/endpoints/auth/accept.ts | 2 +- .../src/server/api/endpoints/i/update-email.ts | 4 +-- .../backend/src/server/api/endpoints/invite.ts | 7 +++-- .../src/server/api/endpoints/miauth/gen-token.ts | 2 +- .../server/api/endpoints/request-reset-password.ts | 6 ++--- packages/backend/test/e2e/move.ts | 14 +++++----- packages/backend/test/unit/RoleService.ts | 30 +++++++++++----------- packages/backend/test/unit/activitypub.ts | 10 ++++---- pnpm-lock.yaml | 3 --- 16 files changed, 57 insertions(+), 64 deletions(-) (limited to 'packages/backend/test/e2e') diff --git a/packages/backend/package.json b/packages/backend/package.json index b9995d8117..c13f292c76 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -133,7 +133,6 @@ "redis-lock": "0.1.4", "reflect-metadata": "0.1.13", "rename": "1.0.4", - "rndstr": "1.0.0", "rss-parser": "3.13.0", "rxjs": "7.8.1", "s-age": "1.1.2", diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/generate-native-user-token.ts index 5d8a4c5378..7292d765a8 100644 --- a/packages/backend/src/misc/generate-native-user-token.ts +++ b/packages/backend/src/misc/generate-native-user-token.ts @@ -1,3 +1,3 @@ import { secureRndstr } from '@/misc/secure-rndstr.js'; -export default () => secureRndstr(16, true); +export default () => secureRndstr(16); diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts index 8d4fcb1ba9..cde64c8142 100644 --- a/packages/backend/src/misc/secure-rndstr.ts +++ b/packages/backend/src/misc/secure-rndstr.ts @@ -1,10 +1,9 @@ import * as crypto from 'node:crypto'; -const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; +export const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; -export function secureRndstr(length = 32, useLU = true): string { - const chars = useLU ? LU_CHARS : L_CHARS; +export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string { const chars_len = chars.length; let str = ''; diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index b2bd7d82e7..fc5f3811eb 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import rndstr from 'rndstr'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; @@ -16,6 +15,7 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { bindThis } from '@/decorators.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; @Injectable() export class SignupApiService { @@ -67,7 +67,7 @@ export class SignupApiService { const body = request.body; const instance = await this.metaService.fetch(true); - + // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { @@ -76,7 +76,7 @@ export class SignupApiService { throw new FastifyReplyError(400, err); }); } - + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); @@ -89,44 +89,44 @@ export class SignupApiService { }); } } - + const username = body['username']; const password = body['password']; const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null; const invitationCode = body['invitationCode']; const emailAddress = body['emailAddress']; - + if (instance.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); return; } - + const res = await this.emailService.validateEmailForAccount(emailAddress); if (!res.available) { reply.code(400); return; } } - + if (instance.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; } - + const ticket = await this.registrationTicketsRepository.findOneBy({ code: invitationCode, }); - + if (ticket == null) { reply.code(400); return; } - + this.registrationTicketsRepository.delete(ticket.id); } - + if (instance.emailRequiredForSignup) { if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); @@ -142,7 +142,7 @@ export class SignupApiService { throw new FastifyReplyError(400, 'DENIED_USERNAME'); } - const code = rndstr('a-z0-9', 16); + const code = secureRndstr(16, { chars: L_CHARS }); // Generate hash of password const salt = await bcrypt.genSalt(8); @@ -170,12 +170,12 @@ export class SignupApiService { const { account, secret } = await this.signupService.signup({ username, password, host, }); - + const res = await this.userEntityService.pack(account, account, { detail: true, includeSecrets: true, }); - + return { ...res, token: secret, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 509224e9c3..2fcf0da3f0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import rndstr from 'rndstr'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { DriveFilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index d263f99f6e..e9c3b0e69f 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -1,9 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; -import rndstr from 'rndstr'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; export const meta = { tags: ['admin'], @@ -54,7 +54,7 @@ export default class extends Endpoint { throw new Error('cannot reset password of root'); } - const passwd = rndstr('a-zA-Z0-9', 8); + const passwd = secureRndstr(8); // Generate hash of password const hash = bcrypt.hashSync(passwd); diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index c1d0a9dd74..aaef02d03f 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -44,7 +44,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { // Generate secret - const secret = secureRndstr(32, true); + const secret = secureRndstr(32); // for backward compatibility const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index 05842460cf..e69f9c12e2 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -55,7 +55,7 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchSession); } - const accessToken = secureRndstr(32, true); + const accessToken = secureRndstr(32); // Fetch exist access token const exist = await this.accessTokensRepository.findOneBy({ diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 4f543a6472..58e056bd37 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import rndstr from 'rndstr'; import ms from 'ms'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -9,6 +8,7 @@ import { EmailService } from '@/core/EmailService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -94,7 +94,7 @@ export default class extends Endpoint { this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj); if (ps.email != null) { - const code = rndstr('a-z0-9', 16); + const code = secureRndstr(16, { chars: L_CHARS }); await this.userProfilesRepository.update(me.id, { emailVerifyCode: code, diff --git a/packages/backend/src/server/api/endpoints/invite.ts b/packages/backend/src/server/api/endpoints/invite.ts index 5d2c479e79..276adcb07f 100644 --- a/packages/backend/src/server/api/endpoints/invite.ts +++ b/packages/backend/src/server/api/endpoints/invite.ts @@ -1,9 +1,9 @@ -import rndstr from 'rndstr'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RegistrationTicketsRepository } from '@/models/index.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; export const meta = { tags: ['meta'], @@ -42,9 +42,8 @@ export default class extends Endpoint { private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const code = rndstr({ - length: 8, - chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns) + const code = secureRndstr(8, { + chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns) }); await this.registrationTicketsRepository.insert({ diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index 97def86262..0ea29f04dc 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -49,7 +49,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { // Generate access token - const accessToken = secureRndstr(32, true); + const accessToken = secureRndstr(32); const now = new Date(); diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts index 3b6ebfe281..284ed8410d 100644 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ b/packages/backend/src/server/api/endpoints/request-reset-password.ts @@ -1,4 +1,3 @@ -import rndstr from 'rndstr'; import ms from 'ms'; import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; @@ -8,6 +7,7 @@ import { IdService } from '@/core/IdService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { EmailService } from '@/core/EmailService.js'; +import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; export const meta = { tags: ['reset password'], @@ -41,7 +41,7 @@ export default class extends Endpoint { constructor( @Inject(DI.config) private config: Config, - + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -77,7 +77,7 @@ export default class extends Endpoint { return; } - const token = rndstr('a-z0-9', 64); + const token = secureRndstr(64, { chars: L_CHARS }); await this.passwordResetRequestsRepository.insert({ id: this.idService.genId(), diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index cd9459fa52..2fefcd0f0e 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -1,10 +1,10 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import rndstr from 'rndstr'; import { loadConfig } from '@/config.js'; import { User, UsersRepository } from '@/models/index.js'; import { jobQueue } from '@/boot/common.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -163,7 +163,7 @@ describe('Account Move', () => { alsoKnownAs: [`@alice@${url.hostname}`], }, root); const listRoot = await api('/users/lists/create', { - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), }, root); await api('/users/lists/push', { listId: listRoot.body.id, @@ -177,9 +177,9 @@ describe('Account Move', () => { userId: eve.id, }, alice); const antenna = await api('/antennas/create', { - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), src: 'home', - keywords: [rndstr('0-9a-z', 8)], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [], caseSensitive: false, @@ -211,7 +211,7 @@ describe('Account Move', () => { userId: dave.id, }, eve); const listEve = await api('/users/lists/create', { - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), }, eve); await api('/users/lists/push', { listId: listEve.body.id, @@ -420,9 +420,9 @@ describe('Account Move', () => { test('Prohibit access after moving: /antennas/update', async () => { const res = await api('/antennas/update', { antennaId, - name: rndstr('0-9a-z', 8), + name: secureRndstr(8), src: 'users', - keywords: [rndstr('0-9a-z', 8)], + keywords: [secureRndstr(8)], excludeKeywords: [], users: [eve.id], caseSensitive: false, diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 907f1f2edc..6979f23e0c 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -4,7 +4,6 @@ import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; -import rndstr from 'rndstr'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js'; @@ -14,6 +13,7 @@ import { genAid } from '@/misc/id/aid.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; import { sleep } from '../utils.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -30,7 +30,7 @@ describe('RoleService', () => { let clock: lolex.InstalledClock; function createUser(data: Partial = {}) { - const un = rndstr('a-z0-9', 16); + const un = secureRndstr(16); return usersRepository.insert({ id: genAid(new Date()), createdAt: new Date(), @@ -106,19 +106,19 @@ describe('RoleService', () => { }); describe('getUserPolicies', () => { - test('instance default policies', async () => { + test('instance default policies', async () => { const user = await createUser(); metaService.fetch.mockResolvedValue({ policies: { canManageCustomEmojis: false, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.canManageCustomEmojis).toBe(false); }); - + test('instance default policies 2', async () => { const user = await createUser(); metaService.fetch.mockResolvedValue({ @@ -126,12 +126,12 @@ describe('RoleService', () => { canManageCustomEmojis: true, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.canManageCustomEmojis).toBe(true); }); - + test('with role', async () => { const user = await createUser(); const role = await createRole({ @@ -150,9 +150,9 @@ describe('RoleService', () => { canManageCustomEmojis: false, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.canManageCustomEmojis).toBe(true); }); @@ -185,9 +185,9 @@ describe('RoleService', () => { driveCapacityMb: 50, }, } as any); - + const result = await roleService.getUserPolicies(user.id); - + expect(result.driveCapacityMb).toBe(100); }); @@ -226,7 +226,7 @@ describe('RoleService', () => { canManageCustomEmojis: false, }, } as any); - + const user1Policies = await roleService.getUserPolicies(user1.id); const user2Policies = await roleService.getUserPolicies(user2.id); expect(user1Policies.canManageCustomEmojis).toBe(false); @@ -251,7 +251,7 @@ describe('RoleService', () => { canManageCustomEmojis: false, }, } as any); - + const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 146998937e..7cd740a2fa 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -1,7 +1,6 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import rndstr from 'rndstr'; import { Test } from '@nestjs/testing'; import { jest } from '@jest/globals'; @@ -13,13 +12,14 @@ import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { IActor } from '@/core/activitypub/type.js'; -import { MockResolver } from '../misc/mock-resolver.js'; import { Note } from '@/models/index.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { MockResolver } from '../misc/mock-resolver.js'; const host = 'https://host1.test'; function createRandomActor(): IActor & { id: string } { - const preferredUsername = `${rndstr('A-Z', 4)}${rndstr('a-z', 4)}`; + const preferredUsername = secureRndstr(8); const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; return { @@ -61,7 +61,7 @@ describe('ActivityPub', () => { const post = { '@context': 'https://www.w3.org/ns/activitystreams', - id: `${host}/users/${rndstr('0-9a-z', 8)}`, + id: `${host}/users/${secureRndstr(8)}`, type: 'Note', attributedTo: actor.id, to: 'https://www.w3.org/ns/activitystreams#Public', @@ -94,7 +94,7 @@ describe('ActivityPub', () => { test('Truncate long name', async () => { const actor = { ...createRandomActor(), - name: rndstr('0-9a-z', 129), + name: secureRndstr(129), }; resolver._register(actor.id, actor); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97f9a9583c..893e5409b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -314,9 +314,6 @@ importers: rename: specifier: 1.0.4 version: 1.0.4 - rndstr: - specifier: 1.0.0 - version: 1.0.0 rss-parser: specifier: 3.13.0 version: 3.13.0 -- cgit v1.2.3-freya From d23ad8b5117a46067464d1b693ae8898d127d5fc Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Mon, 26 Jun 2023 03:09:12 +0200 Subject: fix(backend): APIエラーのHTTP status code変更 (#11047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/ApiCallService.ts | 8 +++-- packages/backend/test/e2e/api.ts | 44 ++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) (limited to 'packages/backend/test/e2e') diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index dad1a4132a..45fb473763 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -83,7 +83,7 @@ export class ApiCallService implements OnApplicationShutdown { } }).catch(err => { if (err instanceof AuthenticationError) { - this.send(reply, 403, new ApiError({ + this.send(reply, 401, new ApiError({ message: 'Authentication failed. Please ensure your token is correct.', code: 'AUTHENTICATION_FAILED', id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', @@ -137,7 +137,7 @@ export class ApiCallService implements OnApplicationShutdown { } }).catch(err => { if (err instanceof AuthenticationError) { - this.send(reply, 403, new ApiError({ + this.send(reply, 401, new ApiError({ message: 'Authentication failed. Please ensure your token is correct.', code: 'AUTHENTICATION_FAILED', id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', @@ -278,6 +278,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You are not assigned to a moderator role.', code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', id: 'd33d5333-db36-423d-a8f9-1a2b9549da41', }); } @@ -285,6 +286,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You are not assigned to an administrator role.', code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', id: 'c3d38592-54c0-429d-be96-5636b0431a61', }); } @@ -296,6 +298,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You are not assigned to a required role.', code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a', }); } @@ -305,6 +308,7 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'Your app does not have the necessary permissions to use this endpoint.', code: 'PERMISSION_DENIED', + kind: 'permission', id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', }); } diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 194ded7e8b..4b9167b6b1 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer } from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; @@ -81,4 +81,46 @@ describe('API', () => { assert.strictEqual(res.body.nullableDefault, 'hello'); }); }); + + test('管理者専用のAPIのアクセス制限', async () => { + // aliceは管理者、APIを使える + await successfulApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: alice, + }); + + // bobは一般ユーザーだからダメ + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: bob, + }, { + status: 403, + code: 'ROLE_PERMISSION_DENIED', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }); + + // publicアクセスももちろんダメ + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: undefined, + }, { + status: 401, + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + }); + + // ごまがしもダメ + await failedApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { token: 'tsukawasete' }, + }, { + status: 401, + code: 'AUTHENTICATION_FAILED', + id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', + }); + }); }); -- cgit v1.2.3-freya From 1b1f82a2e26ddabd8bdf400760a817acbf290157 Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Wed, 28 Jun 2023 06:37:13 +0200 Subject: feat(backend): accept OAuth bearer token (#11052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(backend): accept OAuth bearer token * refactor * Update packages/backend/src/server/api/ApiCallService.ts Co-authored-by: Acid Chicken (硫酸鶏) * Update packages/backend/src/server/api/ApiCallService.ts Co-authored-by: Acid Chicken (硫酸鶏) * fix * kind: permission for account moved error * also for suspended error * Update packages/backend/src/server/api/StreamingApiServerService.ts Co-authored-by: Acid Chicken (硫酸鶏) --------- Co-authored-by: Acid Chicken (硫酸鶏) Co-authored-by: syuilo --- packages/backend/src/server/api/ApiCallService.ts | 81 +++++++++++------- .../src/server/api/StreamingApiServerService.ts | 14 ++- packages/backend/test/e2e/api.ts | 99 +++++++++++++++++++++- packages/backend/test/utils.ts | 57 +++++++++---- packages/misskey-js/etc/misskey-js.api.md | 12 ++- packages/misskey-js/src/api.types.ts | 11 ++- 6 files changed, 222 insertions(+), 52 deletions(-) (limited to 'packages/backend/test/e2e') diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 45fb473763..09e3724394 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -53,44 +53,72 @@ export class ApiCallService implements OnApplicationShutdown { }, 1000 * 60 * 60); } + #sendApiError(reply: FastifyReply, err: ApiError): void { + let statusCode = err.httpStatusCode; + if (err.httpStatusCode === 401) { + reply.header('WWW-Authenticate', 'Bearer realm="Misskey"'); + } else if (err.kind === 'client') { + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`); + statusCode = statusCode ?? 400; + } else if (err.kind === 'permission') { + // (ROLE_PERMISSION_DENIEDは関係ない) + if (err.code === 'PERMISSION_DENIED') { + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`); + } + statusCode = statusCode ?? 403; + } else if (!statusCode) { + statusCode = 500; + } + this.send(reply, statusCode, err); + } + + #sendAuthenticationError(reply: FastifyReply, err: unknown): void { + if (err instanceof AuthenticationError) { + const message = 'Authentication failed. Please ensure your token is correct.'; + reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_token", error_description="${message}"`); + this.send(reply, 401, new ApiError({ + message: 'Authentication failed. Please ensure your token is correct.', + code: 'AUTHENTICATION_FAILED', + id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', + })); + } else { + this.send(reply, 500, new ApiError()); + } + } + @bindThis public handleRequest( endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record | undefined, Querystring: Record }>, reply: FastifyReply, - ) { + ): void { const body = request.method === 'GET' ? request.query : request.body; - const token = body?.['i']; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : body?.['i']; if (token != null && typeof token !== 'string') { reply.code(400); return; } this.authenticateService.authenticate(token).then(([user, app]) => { this.call(endpoint, user, app, body, null, request).then((res) => { - if (request.method === 'GET' && endpoint.meta.cacheSec && !body?.['i'] && !user) { + if (request.method === 'GET' && endpoint.meta.cacheSec && !token && !user) { reply.header('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); } this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); + this.#sendApiError(reply, err); }); if (user) { this.logIp(request, user); } }).catch(err => { - if (err instanceof AuthenticationError) { - this.send(reply, 401, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - this.send(reply, 500, new ApiError()); - } + this.#sendAuthenticationError(reply, err); }); } @@ -99,7 +127,7 @@ export class ApiCallService implements OnApplicationShutdown { endpoint: IEndpoint & { exec: any }, request: FastifyRequest<{ Body: Record, Querystring: Record }>, reply: FastifyReply, - ) { + ): Promise { const multipartData = await request.file().catch(() => { /* Fastify throws if the remote didn't send multipart data. Return 400 below. */ }); @@ -117,7 +145,10 @@ export class ApiCallService implements OnApplicationShutdown { fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; } - const token = fields['i']; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : fields['i']; if (token != null && typeof token !== 'string') { reply.code(400); return; @@ -129,22 +160,14 @@ export class ApiCallService implements OnApplicationShutdown { }, request).then((res) => { this.send(reply, res); }).catch((err: ApiError) => { - this.send(reply, err.httpStatusCode ? err.httpStatusCode : err.kind === 'client' ? 400 : err.kind === 'permission' ? 403 : 500, err); + this.#sendApiError(reply, err); }); if (user) { this.logIp(request, user); } }).catch(err => { - if (err instanceof AuthenticationError) { - this.send(reply, 401, new ApiError({ - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - })); - } else { - this.send(reply, 500, new ApiError()); - } + this.#sendAuthenticationError(reply, err); }); } @@ -213,7 +236,7 @@ export class ApiCallService implements OnApplicationShutdown { } if (ep.meta.limit) { - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. + // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. let limitActor: string; if (user) { limitActor = user.id; @@ -255,8 +278,8 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'Your account has been suspended.', code: 'YOUR_ACCOUNT_SUSPENDED', + kind: 'permission', id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370', - httpStatusCode: 403, }); } } @@ -266,8 +289,8 @@ export class ApiCallService implements OnApplicationShutdown { throw new ApiError({ message: 'You have moved your account.', code: 'YOUR_ACCOUNT_MOVED', + kind: 'permission', id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31', - httpStatusCode: 403, }); } } @@ -321,7 +344,7 @@ export class ApiCallService implements OnApplicationShutdown { try { data[k] = JSON.parse(data[k]); } catch (e) { - throw new ApiError({ + throw new ApiError({ message: 'Invalid param.', code: 'INVALID_PARAM', id: '0b5f1631-7c1a-41a6-b399-cce335f34d85', diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 8f2e51d584..4a0342d2b4 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -58,11 +58,21 @@ export class StreamingApiServerService { let user: LocalUser | null = null; let app: AccessToken | null = null; + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 + // Note that the standard WHATWG WebSocket API does not support setting any headers, + // but non-browser apps may still be able to set it. + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : q.get('i'); + try { - [user, app] = await this.authenticateService.authenticate(q.get('i')); + [user, app] = await this.authenticateService.authenticate(token); } catch (e) { if (e instanceof AuthenticationError) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.write([ + 'HTTP/1.1 401 Unauthorized', + 'WWW-Authenticate: Bearer realm="Misskey", error="invalid_token", error_description="Failed to authenticate"', + ].join('\r\n') + '\r\n\r\n'); } else { socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n'); } diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 4b9167b6b1..c6beec4f88 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,9 +1,10 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer, successfulApiCall, failedApiCall } from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; +import { IncomingMessage } from 'http'; describe('API', () => { let app: INestApplicationContext; @@ -123,4 +124,100 @@ describe('API', () => { id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', }); }); + + describe('Authentication header', () => { + test('一般リクエスト', async () => { + await successfulApiCall({ + endpoint: '/admin/get-index-stats', + parameters: {}, + user: { + token: alice.token, + bearer: true, + }, + }); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile({ + token: alice.token, + bearer: true, + }); + assert.strictEqual(result.status, 200); + }); + + test('streaming', async () => { + const fired = await waitFire( + { + token: alice.token, + bearer: true, + }, + 'homeTimeline', + () => api('notes/create', { text: 'foo' }, alice), + msg => msg.type === 'note' && msg.body.text === 'foo', + ); + assert.strictEqual(fired, true); + }); + }); + + describe('tokenエラー応答でWWW-Authenticate headerを送る', () => { + describe('invalid_token', () => { + test('一般リクエスト', async () => { + const result = await api('/admin/get-index-stats', {}, { + token: 'syuilo', + bearer: true, + }); + assert.strictEqual(result.status, 401); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile({ + token: 'syuilo', + bearer: true, + }); + assert.strictEqual(result.status, 401); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + }); + + test('streaming', async () => { + await assert.rejects(connectStream( + { + token: 'syuilo', + bearer: true, + }, + 'homeTimeline', + () => { }, + ), (err: IncomingMessage) => { + assert.strictEqual(err.statusCode, 401); + assert.ok(err.headers['www-authenticate']?.startsWith('Bearer realm="Misskey", error="invalid_token", error_description')); + return true; + }); + }); + }); + + describe('tokenがないとrealmだけおくる', () => { + test('一般リクエスト', async () => { + const result = await api('/admin/get-index-stats', {}); + assert.strictEqual(result.status, 401); + assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); + }); + + test('multipartリクエスト', async () => { + const result = await uploadFile(); + assert.strictEqual(result.status, 401); + assert.strictEqual(result.headers.get('WWW-Authenticate'), 'Bearer realm="Misskey"'); + }); + }); + + test('invalid_request', async () => { + const result = await api('/notes/create', { text: true }, { + token: alice.token, + bearer: true, + }); + assert.strictEqual(result.status, 400); + assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description')); + }); + + // TODO: insufficient_scope test (authテストが全然なくて書けない) + }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 8583f024cb..48947072e3 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -2,7 +2,7 @@ import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; import { inspect } from 'node:util'; -import WebSocket from 'ws'; +import WebSocket, { ClientOptions } from 'ws'; import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; @@ -13,7 +13,10 @@ import type * as misskey from 'misskey-js'; export { server as startServer } from '@/boot/common.js'; -interface UserToken { token: string } +interface UserToken { + token: string; + bearer?: boolean; +} const config = loadConfig(); export const port = config.port; @@ -57,27 +60,33 @@ export const failedApiCall = async (request: ApiRequest, assertion: { return res.body; }; -const request = async (path: string, params: any, me?: UserToken): Promise<{ body: any, status: number }> => { - const auth = me ? { - i: me.token, - } : {}; +const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { + const bodyAuth: Record = {}; + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (me?.bearer) { + headers.Authorization = `Bearer ${me.token}`; + } else if (me) { + bodyAuth.i = me.token; + } const res = await relativeFetch(path, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(Object.assign(auth, params)), + headers, + body: JSON.stringify(Object.assign(bodyAuth, params)), redirect: 'manual', }); - const status = res.status; const body = res.headers.get('content-type') === 'application/json; charset=utf-8' ? await res.json() : null; return { - body, status, + status: res.status, + headers: res.headers, + body, }; }; @@ -241,7 +250,7 @@ interface UploadOptions { * Upload file * @param user User */ -export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOptions = {}): Promise => { +export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { const absPath = path == null ? new URL('resources/Lenna.jpg', import.meta.url) : isAbsolute(path.toString()) @@ -249,7 +258,6 @@ export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOp : new URL(path, new URL('resources/', import.meta.url)); const formData = new FormData(); - formData.append('i', user.token); formData.append('file', blob ?? new File([await readFile(absPath)], basename(absPath.toString()))); formData.append('force', 'true'); @@ -257,15 +265,24 @@ export const uploadFile = async (user: UserToken, { path, name, blob }: UploadOp formData.append('name', name); } + const headers: Record = {}; + if (user?.bearer) { + headers.Authorization = `Bearer ${user.token}`; + } else if (user) { + formData.append('i', user.token); + } + const res = await relativeFetch('api/drive/files/create', { method: 'POST', body: formData, + headers, }); - const body = res.status !== 204 ? await res.json() : null; + const body = res.status !== 204 ? await res.json() as misskey.Endpoints['drive/files/create']['res'] : null; return { status: res.status, + headers: res.headers, body, }; }; @@ -294,8 +311,16 @@ export const uploadUrl = async (user: UserToken, url: string) => { export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { return new Promise((res, rej) => { - const ws = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${user.token}`); + const url = new URL(`ws://127.0.0.1:${port}/streaming`); + const options: ClientOptions = {}; + if (user.bearer) { + options.headers = { Authorization: `Bearer ${user.token}` }; + } else { + url.searchParams.set('i', user.token); + } + const ws = new WebSocket(url, options); + ws.on('unexpected-response', (req, res) => rej(res)); ws.on('open', () => { ws.on('message', data => { const msg = JSON.parse(data.toString()); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5f292148ae..c9b3fd6056 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -960,8 +960,14 @@ export type Endpoints = { res: TODO; }; 'drive/files/create': { - req: TODO; - res: TODO; + req: { + folderId?: string; + name?: string; + comment?: string; + isSentisive?: boolean; + force?: boolean; + }; + res: DriveFile; }; 'drive/files/delete': { req: { @@ -2750,7 +2756,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:611:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:620:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index 293e0043b7..93f327e67e 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -262,7 +262,16 @@ export type Endpoints = { 'drive/files': { req: { folderId?: DriveFolder['id'] | null; type?: DriveFile['type'] | null; limit?: number; sinceId?: DriveFile['id']; untilId?: DriveFile['id']; }; res: DriveFile[]; }; 'drive/files/attached-notes': { req: TODO; res: TODO; }; 'drive/files/check-existence': { req: TODO; res: TODO; }; - 'drive/files/create': { req: TODO; res: TODO; }; + 'drive/files/create': { + req: { + folderId?: string, + name?: string, + comment?: string, + isSentisive?: boolean, + force?: boolean, + }; + res: DriveFile; + }; 'drive/files/delete': { req: { fileId: DriveFile['id']; }; res: null; }; 'drive/files/find-by-hash': { req: TODO; res: TODO; }; 'drive/files/find': { req: { name: string; folderId?: DriveFolder['id'] | null; }; res: DriveFile[]; }; -- cgit v1.2.3-freya