mirror of
https://activitypub.software/TransFem-org/Sharkey.git
synced 2024-12-13 17:30:45 +01:00
test(server): add validation test of api:notes/create (#10090)
* fix(server): notes/createのバリデーションが効いていない Fix #10079 Co-Authored-By: mei23 <m@m544.net> * anyOf内にバリデーションを書いても最初の一つしかチェックされない * ✌️ * wip * wip * ✌️ * RequiredProp * Revert "RequiredProp" This reverts commit 74693900119a590263106fa3adefd008d69ce80c. * add api:notes/create * fix lint * text * ✌️ * improve readability --------- Co-authored-by: mei23 <m@m544.net> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
dbd9d11d67
commit
18dbcfa0b0
19 changed files with 424 additions and 212 deletions
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -2,5 +2,8 @@
|
||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"**/node_modules": true
|
"**/node_modules": true
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"files.associations": {
|
||||||
|
"*.test.ts": "typescript"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -299,6 +299,27 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||||
- 生成後、ファイルをmigration下に移してください
|
- 生成後、ファイルを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`せよ
|
### コネクションには`markRaw`せよ
|
||||||
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。
|
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
project: ['./tsconfig.json'],
|
project: ['./tsconfig.json', './test/tsconfig.json'],
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'../shared/.eslintrc.js',
|
'../shared/.eslintrc.js',
|
||||||
|
|
|
@ -20,7 +20,7 @@ module.exports = {
|
||||||
// collectCoverage: false,
|
// collectCoverage: false,
|
||||||
|
|
||||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
// 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
|
// The directory where Jest should output its coverage files
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
@ -159,6 +159,7 @@ module.exports = {
|
||||||
// The glob patterns Jest uses to detect test files
|
// The glob patterns Jest uses to detect test files
|
||||||
testMatch: [
|
testMatch: [
|
||||||
"<rootDir>/test/unit/**/*.ts",
|
"<rootDir>/test/unit/**/*.ts",
|
||||||
|
"<rootDir>/src/**/*.test.ts",
|
||||||
//"<rootDir>/test/e2e/**/*.ts"
|
//"<rootDir>/test/e2e/**/*.ts"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -116,10 +116,10 @@ export type Obj = Record<string, Schema>;
|
||||||
// https://github.com/misskey-dev/misskey/issues/8535
|
// https://github.com/misskey-dev/misskey/issues/8535
|
||||||
// To avoid excessive stack depth error,
|
// To avoid excessive stack depth error,
|
||||||
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
|
// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it).
|
||||||
export type ObjType<s extends Obj, RequiredProps extends keyof s> =
|
export type ObjType<s extends Obj, RequiredProps extends ReadonlyArray<keyof s>> =
|
||||||
UnionToIntersection<
|
UnionToIntersection<
|
||||||
{ -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } &
|
{ -readonly [R in RequiredPropertyNames<s>]-?: SchemaType<s[R]> } &
|
||||||
{ -readonly [R in RequiredProps]-?: SchemaType<s[R]> } &
|
{ -readonly [R in RequiredProps[number]]-?: SchemaType<s[R]> } &
|
||||||
{ -readonly [P in keyof s]?: SchemaType<s[P]> }
|
{ -readonly [P in keyof s]?: SchemaType<s[P]> }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -136,18 +136,19 @@ type PartialIntersection<T> = Partial<UnionToIntersection<T>>;
|
||||||
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
|
// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552
|
||||||
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
|
// To get union, we use `Foo extends any ? Hoge<Foo> : never`
|
||||||
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
|
type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? SchemaType<X> : never;
|
||||||
type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
//type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never;
|
||||||
|
type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never;
|
||||||
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
type ArrayUnion<T> = T extends any ? Array<T> : never;
|
||||||
|
|
||||||
type ObjectSchemaTypeDef<p extends Schema> =
|
type ObjectSchemaTypeDef<p extends Schema> =
|
||||||
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
p['ref'] extends keyof typeof refs ? Packed<p['ref']> :
|
||||||
p['properties'] extends NonNullable<Obj> ?
|
p['properties'] extends NonNullable<Obj> ?
|
||||||
p['anyOf'] extends ReadonlyArray<Schema> ?
|
p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
|
||||||
ObjType<p['properties'], NonNullable<p['required']>[number]> & UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>>
|
UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
|
||||||
:
|
: never
|
||||||
ObjType<p['properties'], NonNullable<p['required']>[number]>
|
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||||
:
|
:
|
||||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionObjectSchemaType<p['anyOf']> & PartialIntersection<UnionObjectSchemaType<p['anyOf']>> :
|
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||||
any
|
any
|
||||||
|
|
||||||
|
|
|
@ -138,19 +138,13 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fileId: { type: 'string', format: 'misskey:id' },
|
||||||
|
url: { type: 'string' },
|
||||||
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{ required: ['fileId'] },
|
||||||
properties: {
|
{ required: ['url'] },
|
||||||
fileId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: ['fileId'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
properties: {
|
|
||||||
url: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['url'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -39,19 +39,13 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fileId: { type: 'string', format: 'misskey:id' },
|
||||||
|
url: { type: 'string' },
|
||||||
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{ required: ['fileId'] },
|
||||||
properties: {
|
{ required: ['url'] },
|
||||||
fileId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: ['fileId'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
properties: {
|
|
||||||
url: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['url'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
248
packages/backend/src/server/api/endpoints/notes/create.test.ts
Normal file
248
packages/backend/src/server/api/endpoints/notes/create.test.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -101,74 +101,55 @@ export const paramDef = {
|
||||||
noExtractHashtags: { type: 'boolean', default: false },
|
noExtractHashtags: { type: 'boolean', default: false },
|
||||||
noExtractEmojis: { type: 'boolean', default: false },
|
noExtractEmojis: { type: 'boolean', default: false },
|
||||||
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
|
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
channelId: { 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: [
|
anyOf: [
|
||||||
{
|
{ required: ['text'] },
|
||||||
// (re)note with text, files and poll are optional
|
{ required: ['fileIds'] },
|
||||||
properties: {
|
{ required: ['mediaIds'] },
|
||||||
text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
|
{ required: ['poll'] },
|
||||||
},
|
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -36,32 +36,25 @@ export const paramDef = {
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
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: [
|
anyOf: [
|
||||||
{
|
{ required: ['tag'] },
|
||||||
properties: {
|
{ required: ['query'] },
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -29,20 +29,14 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
pageId: { type: 'string', format: 'misskey:id' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
username: { type: 'string' },
|
||||||
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{ required: ['pageId'] },
|
||||||
properties: {
|
{ required: ['name', 'username'] },
|
||||||
pageId: { type: 'string', format: 'misskey:id' },
|
|
||||||
},
|
|
||||||
required: ['pageId'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
properties: {
|
|
||||||
name: { type: 'string' },
|
|
||||||
username: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['name', 'username'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -46,25 +46,18 @@ export const paramDef = {
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
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: [
|
anyOf: [
|
||||||
{
|
{ required: ['userId'] },
|
||||||
properties: {
|
{ required: ['username', 'host'] },
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -46,25 +46,18 @@ export const paramDef = {
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
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: [
|
anyOf: [
|
||||||
{
|
{ required: ['userId'] },
|
||||||
properties: {
|
{ required: ['username', 'host'] },
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -31,20 +31,13 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
detail: { type: 'boolean', default: true },
|
detail: { type: 'boolean', default: true },
|
||||||
|
|
||||||
|
username: { type: 'string', nullable: true },
|
||||||
|
host: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{ required: ['username'] },
|
||||||
properties: {
|
{ required: ['host'] },
|
||||||
username: { type: 'string', nullable: true },
|
|
||||||
},
|
|
||||||
required: ['username'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
properties: {
|
|
||||||
host: { type: 'string', nullable: true },
|
|
||||||
},
|
|
||||||
required: ['host'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -54,32 +54,22 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
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: [
|
anyOf: [
|
||||||
{
|
{ required: ['userId'] },
|
||||||
properties: {
|
{ required: ['userIds'] },
|
||||||
userId: { type: 'string', format: 'misskey:id' },
|
{ required: ['username'] },
|
||||||
},
|
|
||||||
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'],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
11
packages/backend/test/prelude/get-api-validator.ts
Normal file
11
packages/backend/test/prelude/get-api-validator.ts
Normal file
|
@ -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);
|
||||||
|
}
|
BIN
packages/backend/test/resources/misskey.svg
Normal file
BIN
packages/backend/test/resources/misskey.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
|
@ -33,11 +33,12 @@
|
||||||
"lib": [
|
"lib": [
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"types": ["jest"]
|
"types": ["jest", "node"]
|
||||||
},
|
},
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*.ts",
|
"./**/*.ts",
|
||||||
|
"../src/**/*.test.ts",
|
||||||
"../src/@types/**/*.ts",
|
"../src/@types/**/*.ts",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,7 @@
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"./src/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"outDir": "./built",
|
"outDir": "./built",
|
||||||
"types": [
|
"types": [
|
||||||
|
@ -46,4 +44,7 @@
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*.ts"
|
"./src/**/*.ts"
|
||||||
],
|
],
|
||||||
|
"exclude": [
|
||||||
|
"./src/**/*.test.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue