From 18dbcfa0b09715a234a4eca0288e17d5cbf7622c Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 26 Feb 2023 11:28:05 +0900 Subject: [PATCH] test(server): add validation test of api:notes/create (#10090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(server): notes/createのバリデーションが効いていない Fix #10079 Co-Authored-By: mei23 * anyOf内にバリデーションを書いても最初の一つしかチェックされない * :v: * wip * wip * :v: * RequiredProp * Revert "RequiredProp" This reverts commit 74693900119a590263106fa3adefd008d69ce80c. * add api:notes/create * fix lint * text * :v: * improve readability --------- Co-authored-by: mei23 Co-authored-by: syuilo --- .vscode/settings.json | 5 +- CONTRIBUTING.md | 21 ++ packages/backend/.eslintrc.cjs | 2 +- packages/backend/jest.config.cjs | 3 +- packages/backend/src/misc/schema.ts | 17 +- .../api/endpoints/admin/drive/show-file.ts | 18 +- .../server/api/endpoints/drive/files/show.ts | 18 +- .../server/api/endpoints/notes/create.test.ts | 248 ++++++++++++++++++ .../src/server/api/endpoints/notes/create.ts | 111 ++++---- .../api/endpoints/notes/search-by-tag.ts | 41 ++- .../src/server/api/endpoints/pages/show.ts | 20 +- .../server/api/endpoints/users/followers.ts | 27 +- .../server/api/endpoints/users/following.ts | 27 +- .../users/search-by-username-and-host.ts | 17 +- .../src/server/api/endpoints/users/show.ts | 40 ++- .../backend/test/prelude/get-api-validator.ts | 11 + packages/backend/test/resources/misskey.svg | Bin 0 -> 9380 bytes packages/backend/test/tsconfig.json | 3 +- packages/backend/tsconfig.json | 7 +- 19 files changed, 424 insertions(+), 212 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notes/create.test.ts create mode 100644 packages/backend/test/prelude/get-api-validator.ts create mode 100644 packages/backend/test/resources/misskey.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index c94a34194e..6a0497946d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "search.exclude": { "**/node_modules": true }, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "files.associations": { + "*.test.ts": "typescript" + } } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 668989f122..10d93cd9fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -299,6 +299,27 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o - 生成後、ファイルをmigration下に移してください - 作成されたスクリプトは不必要な変更を含むため除去してください +### JSON SchemaのobjectでanyOfを使うとき +JSON Schemaで、objectに対してanyOfを使う場合、anyOfの中でpropertiesを定義しないこと。 +バリデーションが効かないため。(SchemaTypeもそのように作られており、objectのanyOf内のpropertiesは捨てられます) +https://github.com/misskey-dev/misskey/pull/10082 + +テキストhogeおよびfugaについて、片方を必須としつつ両方の指定もありうる場合: + +``` +export const paramDef = { + type: 'object', + properties: { + hoge: { type: 'string', minLength: 1 }, + fuga: { type: 'string', minLength: 1 }, + }, + anyOf: [ + { required: ['hoge'] }, + { required: ['fuga'] }, + ], +} as const; +``` + ### コネクションには`markRaw`せよ **Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。 diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs index 5a06889dcd..f9fe4814e6 100644 --- a/packages/backend/.eslintrc.cjs +++ b/packages/backend/.eslintrc.cjs @@ -1,7 +1,7 @@ module.exports = { parserOptions: { tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], + project: ['./tsconfig.json', './test/tsconfig.json'], }, extends: [ '../shared/.eslintrc.js', diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 2f11f6a3e9..8a11ad848c 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -20,7 +20,7 @@ module.exports = { // collectCoverage: false, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['src/**/*.ts'], + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], // The directory where Jest should output its coverage files coverageDirectory: "coverage", @@ -159,6 +159,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ "/test/unit/**/*.ts", + "/src/**/*.test.ts", //"/test/e2e/**/*.ts" ], diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 7fc4a3e654..6a0802f8a4 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -116,10 +116,10 @@ export type Obj = Record; // https://github.com/misskey-dev/misskey/issues/8535 // To avoid excessive stack depth error, // deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). -export type ObjType = +export type ObjType> = UnionToIntersection< { -readonly [R in RequiredPropertyNames]-?: SchemaType } & - { -readonly [R in RequiredProps]-?: SchemaType } & + { -readonly [R in RequiredProps[number]]-?: SchemaType } & { -readonly [P in keyof s]?: SchemaType } >; @@ -136,18 +136,19 @@ type PartialIntersection = Partial>; // https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 // To get union, we use `Foo extends any ? Hoge : never` type UnionSchemaType = X extends any ? SchemaType : never; -type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; +//type UnionObjectSchemaType = X extends any ? ObjectSchemaType : never; +type UnionObjType = a[number]> = X extends any ? ObjType : never; type ArrayUnion = T extends any ? Array : never; type ObjectSchemaTypeDef

= p['ref'] extends keyof typeof refs ? Packed : p['properties'] extends NonNullable ? - p['anyOf'] extends ReadonlyArray ? - ObjType[number]> & UnionObjectSchemaType & PartialIntersection> - : - ObjType[number]> + p['anyOf'] extends ReadonlyArray ? p['anyOf'][number]['required'] extends ReadonlyArray ? + UnionObjType> & ObjType> + : never + : ObjType> : - p['anyOf'] extends ReadonlyArray ? UnionObjectSchemaType & PartialIntersection> : + p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md p['allOf'] extends ReadonlyArray ? UnionToIntersection> : any diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 85b566aabe..1d27ac2137 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -138,19 +138,13 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + url: { type: 'string' }, + }, anyOf: [ - { - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], - }, - { - properties: { - url: { type: 'string' }, - }, - required: ['url'], - }, + { required: ['fileId'] }, + { required: ['url'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index e0a07a3640..271b33ef4b 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -39,19 +39,13 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + url: { type: 'string' }, + }, anyOf: [ - { - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], - }, - { - properties: { - url: { type: 'string' }, - }, - required: ['url'], - }, + { required: ['fileId'] }, + { required: ['url'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts new file mode 100644 index 0000000000..4e5ec361f9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -0,0 +1,248 @@ +process.env.NODE_ENV = 'test'; + +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import { describe, test, expect } from '@jest/globals'; +import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; +import { paramDef } from './create.js'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); + +const VALID = true; +const INVALID = false; + +describe('api:notes/create', () => { + describe('validation', () => { + const v = getValidator(paramDef); + const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8'); + + test('reject empty', () => { + const valid = v({ }); + expect(valid).toBe(INVALID); + }); + + describe('text', () => { + test('simple post', () => { + expect(v({ text: 'Hello, world!' })) + .toBe(VALID); + }); + + test('null post', () => { + expect(v({ text: null })) + .toBe(INVALID); + }); + + test('0 characters post', () => { + expect(v({ text: '' })) + .toBe(INVALID); + }); + + test('over 3000 characters post', async () => { + expect(v({ text: await tooLong })) + .toBe(INVALID); + }); + }); + + describe('cw', () => { + test('simple cw', () => { + expect(v({ text: 'Hello, world!', cw: 'Hello, world!' })) + .toBe(VALID); + }); + + test('null cw', () => { + expect(v({ text: 'Body', cw: null })) + .toBe(VALID); + }); + + test('0 characters cw', () => { + expect(v({ text: 'Body', cw: '' })) + .toBe(VALID); + }); + + test('reject only cw', () => { + expect(v({ cw: 'Hello, world!' })) + .toBe(INVALID); + }); + + test('over 100 characters cw', async () => { + expect(v({ text: 'Body', cw: await tooLong })) + .toBe(INVALID); + }); + }); + + describe('visibility', () => { + test('public', () => { + expect(v({ text: 'Hello, world!', visibility: 'public' })) + .toBe(VALID); + }); + + test('home', () => { + expect(v({ text: 'Hello, world!', visibility: 'home' })) + .toBe(VALID); + }); + + test('followers', () => { + expect(v({ text: 'Hello, world!', visibility: 'followers' })) + .toBe(VALID); + }); + + test('reject only visibility', () => { + expect(v({ visibility: 'public' })) + .toBe(INVALID); + }); + + test('reject invalid visibility', () => { + expect(v({ text: 'Hello, world!', visibility: 'invalid' })) + .toBe(INVALID); + }); + + test('reject null visibility', () => { + expect(v({ text: 'Hello, world!', visibility: null })) + .toBe(INVALID); + }); + + describe('visibility:specified', () => { + test('specified without visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified' })) + .toBe(VALID); + }); + + test('specified with empty visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: [] })) + .toBe(VALID); + }); + + test('reject specified with non unique visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: ['1', '1', '2'] })) + .toBe(INVALID); + }); + + test('reject specified with null visibleUserIds', () => { + expect(v({ text: 'Hello, world!', visibility: 'specified', visibleUserIds: null })) + .toBe(INVALID); + }); + }); + }); + + describe('fileIds', () => { + test('only fileIds', () => { + expect(v({ fileIds: ['1', '2', '3'] })) + .toBe(VALID); + }); + + test('text and fileIds', () => { + expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'] })) + .toBe(VALID); + }); + + test('reject null fileIds', () => { + expect(v({ fileIds: null })) + .toBe(INVALID); + }); + + test('reject text and null fileIds (複合的なanyOfのバリデーションが正しく動作する)', () => { + expect(v({ text: 'Hello, world!', fileIds: null })) + .toBe(INVALID); + }); + + test('reject 0 files', () => { + expect(v({ fileIds: [] })) + .toBe(INVALID); + }); + + test('reject non unique', () => { + expect(v({ fileIds: ['1', '1', '2'] })) + .toBe(INVALID); + }); + + test('reject invalid id', () => { + expect(v({ fileIds: ['あ'] })) + .toBe(INVALID); + }); + + test('reject over 17 files', () => { + const valid = v({ text: 'Hello, world!', fileIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'] }); + expect(valid).toBe(INVALID); + }); + }); + + describe('poll', () => { + test('note with poll', () => { + expect(v({ text: 'Hello, world!', poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('null poll', () => { + expect(v({ text: 'Hello, world!', poll: null })) + .toBe(VALID); + }); + + test('allow only poll', () => { + expect(v({ poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('poll with expiresAt', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiresAt: 1 } })) + .toBe(VALID); + }); + + test('poll with expiredAfter', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 1 } })) + .toBe(VALID); + }); + + test('reject poll without choices', () => { + expect(v({ poll: { } })) + .toBe(INVALID); + }); + + test('reject poll with empty choices', () => { + expect(v({ poll: { choices: [] } })) + .toBe(INVALID); + }); + + test('reject poll with null choices', () => { + expect(v({ poll: { choices: null } })) + .toBe(INVALID); + }); + + test('reject poll with 1 choice', () => { + expect(v({ poll: { choices: ['a'] } })) + .toBe(INVALID); + }); + + test('reject poll with too long choice', async () => { + expect(v({ poll: { choices: [await tooLong, '2'] } })) + .toBe(INVALID); + }); + + test('reject poll with too many choices', () => { + expect(v({ poll: { choices: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'] } })) + .toBe(INVALID); + }); + + test('reject poll with non unique choices', () => { + expect(v({ poll: { choices: ['a', 'a', 'b', 'c'] } })) + .toBe(INVALID); + }); + + test('reject poll with expiredAfter 0', async () => { + expect(v({ poll: { choices: ['a', 'b', 'c'], expiredAfter: 0 } })) + .toBe(INVALID); + }); + }); + + test('text, fileIds and poll', () => { + expect(v({ text: 'Hello, world!', fileIds: ['1', '2', '3'], poll: { choices: ['a', 'b', 'c'] } })) + .toBe(VALID); + }); + + test('text, invalid fileIds and invalid poll', () => { + expect(v({ text: 'Hello, world!', fileIds: ['あ'], poll: { choices: ['a'] } })) + .toBe(INVALID); + }); + }); +}); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index f4c5a84a4f..2848cd7df1 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -101,74 +101,55 @@ export const paramDef = { noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, }, + // (re)note with text, files and poll are optional anyOf: [ - { - // (re)note with text, files and poll are optional - properties: { - text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, - }, - required: ['text'], - }, - { - // (re)note with files, text and poll are optional - properties: { - fileIds: { - type: 'array', - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: 'string', format: 'misskey:id' }, - }, - }, - required: ['fileIds'], - }, - { - // (re)note with files, text and poll are optional - properties: { - mediaIds: { - deprecated: true, - description: 'Use `fileIds` instead. If both are specified, this property is discarded.', - type: 'array', - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: 'string', format: 'misskey:id' }, - }, - }, - required: ['mediaIds'], - }, - { - // (re)note with poll, text and files are optional - properties: { - poll: { - type: 'object', - nullable: true, - properties: { - choices: { - type: 'array', - uniqueItems: true, - minItems: 2, - maxItems: 10, - items: { type: 'string', minLength: 1, maxLength: 50 }, - }, - multiple: { type: 'boolean' }, - expiresAt: { type: 'integer', nullable: true }, - expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, - }, - required: ['choices'], - }, - }, - required: ['poll'], - }, - { - // pure renote - properties: { - renoteId: { type: 'string', format: 'misskey:id', nullable: true }, - }, - required: ['renoteId'], - }, + { required: ['text'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index bcd793ac43..da1a4bcc46 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -36,32 +36,25 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + tag: { type: 'string', minLength: 1 }, + query: { + type: 'array', + description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', + items: { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, + }, + minItems: 1, + }, }, anyOf: [ - { - properties: { - tag: { type: 'string', minLength: 1 }, - }, - required: ['tag'], - }, - { - properties: { - query: { - type: 'array', - description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', - items: { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - minItems: 1, - }, - minItems: 1, - }, - }, - required: ['query'], - }, + { required: ['tag'] }, + { required: ['query'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 651252afbb..bf2b2a431e 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -29,20 +29,14 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + pageId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string' }, + username: { type: 'string' }, + }, anyOf: [ - { - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['pageId'], - }, - { - properties: { - name: { type: 'string' }, - username: { type: 'string' }, - }, - required: ['name', 'username'], - }, + { required: ['pageId'] }, + { required: ['name', 'username'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 17ce920011..97f1310c36 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -46,25 +46,18 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + userId: { type: 'string', format: 'misskey:id' }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, }, anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username', 'host'], - }, + { required: ['userId'] }, + { required: ['username', 'host'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 6dbda0d72f..d406594a2e 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -46,25 +46,18 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + + userId: { type: 'string', format: 'misskey:id' }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, }, anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username', 'host'], - }, + { required: ['userId'] }, + { required: ['username', 'host'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 1cefcf2707..6c340d8fb2 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -31,20 +31,13 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, detail: { type: 'boolean', default: true }, + + username: { type: 'string', nullable: true }, + host: { type: 'string', nullable: true }, }, anyOf: [ - { - properties: { - username: { type: 'string', nullable: true }, - }, - required: ['username'], - }, - { - properties: { - host: { type: 'string', nullable: true }, - }, - required: ['host'], - }, + { required: ['username'] }, + { required: ['host'] }, ], } as const; diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 70258ef009..29f24b045a 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -54,32 +54,22 @@ export const meta = { export const paramDef = { type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + userIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, anyOf: [ - { - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], - }, - { - properties: { - userIds: { type: 'array', uniqueItems: true, items: { - type: 'string', format: 'misskey:id', - } }, - }, - required: ['userIds'], - }, - { - properties: { - username: { type: 'string' }, - host: { - type: 'string', - nullable: true, - description: 'The local host is represented with `null`.', - }, - }, - required: ['username'], - }, + { required: ['userId'] }, + { required: ['userIds'] }, + { required: ['username'] }, ], } as const; diff --git a/packages/backend/test/prelude/get-api-validator.ts b/packages/backend/test/prelude/get-api-validator.ts new file mode 100644 index 0000000000..1f4a2dbc95 --- /dev/null +++ b/packages/backend/test/prelude/get-api-validator.ts @@ -0,0 +1,11 @@ +import { Schema } from '@/misc/schema'; +import Ajv from 'ajv'; + +export const getValidator = (paramDef: Schema) => { + const ajv = new Ajv({ + useDefaults: true, + }); + ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); + + return ajv.compile(paramDef); +} diff --git a/packages/backend/test/resources/misskey.svg b/packages/backend/test/resources/misskey.svg new file mode 100644 index 0000000000000000000000000000000000000000..3fcb2d3ecb186e101d900f405ffc87a241f35495 GIT binary patch literal 9380 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 literal 0 HcmV?d00001 diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index da82ddc4a1..8a024a678b 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -33,11 +33,12 @@ "lib": [ "esnext" ], - "types": ["jest"] + "types": ["jest", "node"] }, "compileOnSave": false, "include": [ "./**/*.ts", + "../src/**/*.test.ts", "../src/@types/**/*.ts", ] } diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 6f335a2442..faadbcdfc6 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -26,9 +26,7 @@ "rootDir": "./src", "baseUrl": "./", "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, "outDir": "./built", "types": [ @@ -46,4 +44,7 @@ "include": [ "./src/**/*.ts" ], + "exclude": [ + "./src/**/*.test.ts" + ] }