mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
chore(repo): remove dead code and useless tests (#3375)
This commit is contained in:
1 parent
7e177a1c90
commit
1cd62eb661
1391 files changed
+783
-120415
No files matched your search
@@ -8,7 +8,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "tsx src/scripts/GenerateSpec.ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"validate": "tsx src/scripts/GenerateSpec.ts --validate-only"
|
||||
},
|
||||
@@ -21,10 +20,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "catalog:",
|
||||
"@fluxer/constants": "workspace:*",
|
||||
"@types/node": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"typescript": "catalog:ts7",
|
||||
"vitest": "catalog:"
|
||||
"typescript": "catalog:ts7"
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {type APIErrorCode, APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ErrorCodeToI18nKey} from '@fluxer/errors/src/i18n/ErrorCodeMappings';
|
||||
import {ERROR_I18N_MESSAGES} from '@fluxer/errors/src/i18n/ErrorI18nMessages';
|
||||
import {OpenAPIGenerator} from '@fluxer/openapi/src/OpenAPIGenerator';
|
||||
import {visitOpenAPISchemaObjects} from '@fluxer/openapi/src/OpenAPISchemaVisitor';
|
||||
import type {OpenAPIDocument, OpenAPISchema} from '@fluxer/openapi/src/OpenAPITypes';
|
||||
import {beforeAll, describe, expect, it} from 'vitest';
|
||||
|
||||
const REPOSITORY_PATH = fileURLToPath(new URL('../../../../', import.meta.url));
|
||||
|
||||
describe('OpenAPI generation from API controllers', () => {
|
||||
let document: OpenAPIDocument;
|
||||
beforeAll(async () => {
|
||||
document = await new OpenAPIGenerator({basePath: REPOSITORY_PATH, routeScope: 'all'}).generate();
|
||||
});
|
||||
|
||||
it('describes the channel body before its channel type is injected', () => {
|
||||
expect(document.paths['/channels/{channel_id}'].patch.requestBody).toMatchObject({
|
||||
required: false,
|
||||
content: {'application/json': {schema: {$ref: '#/components/schemas/ChannelUpdateRequestBody'}}},
|
||||
});
|
||||
const options = document.components.schemas.ChannelUpdateRequestBody.anyOf ?? [];
|
||||
expect(options).toHaveLength(11);
|
||||
const withType = options.filter((option) => option.properties && 'type' in option.properties);
|
||||
expect(withType).toHaveLength(2);
|
||||
for (const option of withType) {
|
||||
expect(option.properties?.type).toMatchObject({anyOf: [{const: 0}, {const: 5}]});
|
||||
}
|
||||
for (const option of options.slice(0, 6)) {
|
||||
expect(option.required).toBeUndefined();
|
||||
}
|
||||
expect(options.slice(6)).toEqual([
|
||||
{$ref: '#/components/schemas/ChannelUpdatePublicThreadRequestBody'},
|
||||
{$ref: '#/components/schemas/ChannelUpdatePrivateThreadRequestBody'},
|
||||
{$ref: '#/components/schemas/ChannelUpdateThreadParentRequestBody'},
|
||||
{$ref: '#/components/schemas/ChannelUpdateForumRequestBody'},
|
||||
{$ref: '#/components/schemas/ChannelUpdateMediaRequestBody'},
|
||||
]);
|
||||
for (const name of [
|
||||
'ChannelUpdateThreadParentRequestBody',
|
||||
'ChannelUpdateForumRequestBody',
|
||||
'ChannelUpdateMediaRequestBody',
|
||||
]) {
|
||||
const schema = document.components.schemas[name];
|
||||
expect(schema['x-fluxer-experiment']).toBe('channel_threads');
|
||||
expect(schema.properties).not.toHaveProperty('type');
|
||||
expect(schema.properties).toHaveProperty('default_auto_archive_duration');
|
||||
}
|
||||
expect(document.components.schemas.ChannelUpdateForumRequestBody.properties).toHaveProperty('available_tags');
|
||||
expect(document.components.schemas.ChannelCreateRequest.oneOf).toEqual(
|
||||
expect.arrayContaining([
|
||||
{$ref: '#/components/schemas/GuildForumChannelCreateRequest'},
|
||||
{$ref: '#/components/schemas/GuildMediaChannelCreateRequest'},
|
||||
]),
|
||||
);
|
||||
for (const name of ['ChannelUpdatePublicThreadRequestBody', 'ChannelUpdatePrivateThreadRequestBody']) {
|
||||
const schema = document.components.schemas[name];
|
||||
expect(schema['x-fluxer-experiment']).toBe('channel_threads');
|
||||
expect(schema.properties).not.toHaveProperty('type');
|
||||
expect(schema.properties).toHaveProperty('archived');
|
||||
expect(schema.required).toBeUndefined();
|
||||
}
|
||||
expect(document.components.schemas.ChannelUpdatePrivateThreadRequestBody.properties).toHaveProperty('invitable');
|
||||
});
|
||||
|
||||
it('documents the bodyless response of thread member settings', () => {
|
||||
expect(document.paths['/channels/{channel_id}/thread-members/@me/settings'].patch.responses['204']).toEqual({
|
||||
description: 'No Content',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps path-supplied voice IDs out of JSON request bodies', () => {
|
||||
expect(document.paths['/admin/voice/regions/{region_id}'].patch.requestBody?.required).toBe(false);
|
||||
expect(document.paths['/admin/voice/regions/{region_id}/servers/{server_id}'].patch.requestBody?.required).toBe(
|
||||
false,
|
||||
);
|
||||
expect(document.components.schemas.UpdateVoiceRegionRequestBody.properties).not.toHaveProperty('id');
|
||||
expect(document.components.schemas.UpdateVoiceServerRequestBody.properties).not.toHaveProperty('region_id');
|
||||
expect(document.components.schemas.UpdateVoiceServerRequestBody.properties).not.toHaveProperty('server_id');
|
||||
expect(document.paths['/admin/voice/regions/{region_id}/servers'].post.requestBody?.required).toBe(true);
|
||||
expect(document.components.schemas.CreateVoiceServerRequestBody.required).toEqual([
|
||||
'server_id',
|
||||
'endpoint',
|
||||
'api_key',
|
||||
'api_secret',
|
||||
]);
|
||||
});
|
||||
|
||||
it('tags every channel thread operation with its experiment', () => {
|
||||
const tagged = Object.values(document.paths)
|
||||
.flatMap((pathItem) => Object.values(pathItem))
|
||||
.filter((operation) => operation['x-fluxer-experiment'] === 'channel_threads')
|
||||
.map((operation) => operation.operationId)
|
||||
.sort();
|
||||
expect(tagged).toEqual(
|
||||
[
|
||||
'start_thread_from_message',
|
||||
'start_thread',
|
||||
'list_guild_active_threads',
|
||||
'list_public_archived_threads',
|
||||
'list_private_archived_threads',
|
||||
'list_joined_private_archived_threads',
|
||||
'list_thread_members',
|
||||
'get_thread_member',
|
||||
'join_thread',
|
||||
'add_thread_member',
|
||||
'leave_thread',
|
||||
'remove_thread_member',
|
||||
'update_thread_member_settings',
|
||||
'search_threads',
|
||||
'get_channel_post_data',
|
||||
'create_forum_tag',
|
||||
'update_forum_tag',
|
||||
'delete_forum_tag',
|
||||
'list_admin_guild_threads',
|
||||
'delete_admin_thread_channel',
|
||||
].sort(),
|
||||
);
|
||||
expect(document.paths['/channels/{channel_id}'].get).not.toHaveProperty('x-fluxer-experiment');
|
||||
});
|
||||
|
||||
it('retains the explicit optional sudo body on channel deletion', () => {
|
||||
expect(document.paths['/channels/{channel_id}'].delete.requestBody).toMatchObject({
|
||||
required: false,
|
||||
content: {'application/json': {schema: {$ref: '#/components/schemas/SudoVerificationSchema'}}},
|
||||
});
|
||||
});
|
||||
|
||||
it('documents bot authentication for the current application endpoint', () => {
|
||||
const operation = document.paths['/applications/@me'].get;
|
||||
expect(operation.security).toEqual([{botToken: []}]);
|
||||
expect(operation.responses).toHaveProperty('401');
|
||||
});
|
||||
|
||||
it('documents full and partial harvest archive downloads as binary ZIP responses', () => {
|
||||
const responses = document.paths['/harvest-downloads/{harvestId}'].get.responses;
|
||||
for (const status of ['200', '206']) {
|
||||
expect(responses[status].content).toEqual({
|
||||
'application/zip': {schema: {$ref: '#/components/schemas/HarvestArchiveResponse'}},
|
||||
});
|
||||
}
|
||||
expect(document.components.schemas.HarvestArchiveResponse).toMatchObject({
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
contentEncoding: 'binary',
|
||||
});
|
||||
expect(responses).not.toHaveProperty('204');
|
||||
});
|
||||
|
||||
it('keeps every desktop download redirect out of the published document', () => {
|
||||
const published = Object.keys(document.paths).filter((path) => path.startsWith('/dl'));
|
||||
expect(published).toEqual([]);
|
||||
});
|
||||
|
||||
it('publishes stream preview images as binary responses', () => {
|
||||
expect(document.paths['/streams/{stream_key}/preview'].get.responses['200'].content).toEqual({
|
||||
'image/*': {schema: {$ref: '#/components/schemas/StreamPreviewResponse'}},
|
||||
});
|
||||
});
|
||||
|
||||
it('documents cache revalidation without a response body', () => {
|
||||
const responses = document.paths['/experiments'].get.responses;
|
||||
expect(responses['200'].content).toEqual({
|
||||
'application/json': {schema: {$ref: '#/components/schemas/ExperimentAssignmentsResponse'}},
|
||||
});
|
||||
expect(responses['304']).toEqual({description: 'Not Modified'});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{path: '/channels/{channel_id}/messages/bulk-delete-mine', method: 'post', status: '202'},
|
||||
{path: '/users/@me/guilds/{guild_id}/messages/bulk-delete-mine', method: 'post', status: '202'},
|
||||
{path: '/users/@me/messages/bulk-delete-mine', method: 'post', status: '202'},
|
||||
{path: '/donations/manage', method: 'get', status: '302'},
|
||||
{path: '/oauth2/token/revoke', method: 'post', status: '200'},
|
||||
])('preserves the bodyless $status response for $path', ({path, method, status}) => {
|
||||
const responses = document.paths[path][method].responses;
|
||||
expect(responses[status]).toEqual({description: 'Success'});
|
||||
expect(responses).not.toHaveProperty('204');
|
||||
});
|
||||
});
|
||||
|
||||
const EU_COUNTRY_CODES = [
|
||||
'AT',
|
||||
'BE',
|
||||
'BG',
|
||||
'HR',
|
||||
'CY',
|
||||
'CZ',
|
||||
'DK',
|
||||
'EE',
|
||||
'FI',
|
||||
'FR',
|
||||
'DE',
|
||||
'GR',
|
||||
'HU',
|
||||
'IE',
|
||||
'IT',
|
||||
'LV',
|
||||
'LT',
|
||||
'LU',
|
||||
'MT',
|
||||
'NL',
|
||||
'PL',
|
||||
'PT',
|
||||
'RO',
|
||||
'SK',
|
||||
'SI',
|
||||
'ES',
|
||||
'SE',
|
||||
];
|
||||
|
||||
function errorCodeMessage(code: APIErrorCode): string {
|
||||
return ERROR_I18N_MESSAGES[ErrorCodeToI18nKey[code]];
|
||||
}
|
||||
|
||||
function resolveRef(document: OpenAPIDocument, schema: OpenAPISchema | undefined): OpenAPISchema {
|
||||
const name = schema?.$ref?.replace('#/components/schemas/', '');
|
||||
expect(name).toBeDefined();
|
||||
return document.components.schemas[name ?? ''];
|
||||
}
|
||||
|
||||
function schemasWithEnum(document: OpenAPIDocument, values: ReadonlyArray<string>): Array<OpenAPISchema> {
|
||||
const found: Array<OpenAPISchema> = [];
|
||||
visitOpenAPISchemaObjects(document, (schema) => {
|
||||
if (Array.isArray(schema.enum) && schema.enum.join(',') === values.join(',')) found.push(schema);
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
describe('published public and admin documents', () => {
|
||||
const documents = new Map<'public' | 'admin', OpenAPIDocument>();
|
||||
function documentFor(scope: 'public' | 'admin'): OpenAPIDocument {
|
||||
const document = documents.get(scope);
|
||||
assert(document, `the ${scope} document was not generated`);
|
||||
return document;
|
||||
}
|
||||
beforeAll(async () => {
|
||||
documents.set(
|
||||
'public',
|
||||
await new OpenAPIGenerator({
|
||||
basePath: REPOSITORY_PATH,
|
||||
routeScope: 'public',
|
||||
schemaTarget: 'draft-2020-12',
|
||||
}).generate(),
|
||||
);
|
||||
documents.set(
|
||||
'admin',
|
||||
await new OpenAPIGenerator({
|
||||
basePath: REPOSITORY_PATH,
|
||||
routeScope: 'admin',
|
||||
schemaTarget: 'openapi-3.0',
|
||||
}).generate(),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['public', 'admin'] as const)(
|
||||
'publishes every API error code with its message in the %s document',
|
||||
(scope) => {
|
||||
const {schemas} = documentFor(scope).components;
|
||||
const codes = Object.values(APIErrorCodes);
|
||||
expect(schemas.APIErrorCode.type).toBe('string');
|
||||
expect(schemas.APIErrorCode.enum).toEqual(codes);
|
||||
expect(schemas.APIErrorCode['x-enumDescriptions']).toEqual(codes.map(errorCodeMessage));
|
||||
for (const name of ['Error', 'ThrottledError']) {
|
||||
const code = schemas[name].properties?.code;
|
||||
expect(code).toMatchObject({type: 'string', description: expect.stringContaining('APIErrorCode')});
|
||||
expect(code).not.toHaveProperty('enum');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it('publishes the EU country codes once and references them from every DSA report body', () => {
|
||||
const document = documentFor('public');
|
||||
const euCountryCode = document.components.schemas.EuCountryCode;
|
||||
expect(euCountryCode.enum).toEqual(EU_COUNTRY_CODES);
|
||||
expect(euCountryCode.type).toBe('string');
|
||||
expect(schemasWithEnum(document, EU_COUNTRY_CODES)).toEqual([euCountryCode]);
|
||||
const body = resolveRef(
|
||||
document,
|
||||
document.paths['/reports/dsa'].post.requestBody?.content['application/json']?.schema,
|
||||
);
|
||||
expect(body.oneOf).toHaveLength(3);
|
||||
for (const branch of body.oneOf ?? []) {
|
||||
const properties = resolveRef(document, branch).properties;
|
||||
expect(properties?.reporter_country_of_residence).toEqual({
|
||||
$ref: '#/components/schemas/EuCountryCode',
|
||||
description: 'EU country code of the reporter residence',
|
||||
});
|
||||
expect(properties).not.toHaveProperty('reporter_fluxer_tag');
|
||||
}
|
||||
});
|
||||
|
||||
it('describes only the report statuses that exist', () => {
|
||||
const status = documentFor('public').components.schemas.ReportResponse.properties?.status;
|
||||
expect(status).toEqual({type: 'string', description: 'Current status of the report (pending, resolved)'});
|
||||
});
|
||||
|
||||
it('declares the token query parameter of the harvest archive download', () => {
|
||||
const operation = documentFor('public').paths['/harvest-downloads/{harvestId}'].get;
|
||||
expect(operation.parameters?.map(({name, in: location, required}) => ({name, in: location, required}))).toEqual([
|
||||
{name: 'harvestId', in: 'path', required: true},
|
||||
{name: 'token', in: 'query', required: true},
|
||||
]);
|
||||
expect(operation.parameters?.[1].schema).toMatchObject({type: 'string'});
|
||||
});
|
||||
|
||||
it('leaves the MFA challenge out of responses that never return one', () => {
|
||||
const document = documentFor('public');
|
||||
const responseSchema = (path: string) =>
|
||||
document.paths[path].post.responses['200'].content?.['application/json']?.schema;
|
||||
expect(document.components.schemas.AuthRegisterResponse).toEqual({
|
||||
anyOf: [
|
||||
{$ref: '#/components/schemas/AuthTokenWithUserIdResponse'},
|
||||
{$ref: '#/components/schemas/AuthRegistrationPendingApprovalResponse'},
|
||||
],
|
||||
});
|
||||
expect(responseSchema('/auth/register')).toEqual({$ref: '#/components/schemas/AuthRegisterResponse'});
|
||||
expect(responseSchema('/auth/email-revert')).toEqual({$ref: '#/components/schemas/AuthTokenWithUserIdResponse'});
|
||||
for (const path of ['/auth/login', '/auth/reset']) {
|
||||
expect(responseSchema(path)).toEqual({$ref: '#/components/schemas/AuthLoginResponse'});
|
||||
}
|
||||
const login = document.components.schemas.AuthLoginResponse.anyOf ?? [];
|
||||
expect(login).toHaveLength(2);
|
||||
expect(login[0]).toEqual({$ref: '#/components/schemas/AuthTokenWithUserIdResponse'});
|
||||
expect(login[1]).toHaveProperty('properties.mfa');
|
||||
});
|
||||
|
||||
it('describes an embed provider with a name and a link only', () => {
|
||||
const document = documentFor('public');
|
||||
expect(Object.keys(document.components.schemas.EmbedProviderResponse.properties ?? {})).toEqual(['name', 'url']);
|
||||
for (const name of ['MessageEmbedResponse', 'MessageEmbedChildResponse']) {
|
||||
const properties = document.components.schemas[name].properties;
|
||||
expect(properties?.provider).toMatchObject({
|
||||
anyOf: [{$ref: '#/components/schemas/EmbedProviderResponse'}, {type: 'null'}],
|
||||
});
|
||||
expect(properties?.author).toMatchObject({
|
||||
anyOf: [{$ref: '#/components/schemas/EmbedAuthorResponse'}, {type: 'null'}],
|
||||
});
|
||||
}
|
||||
expect(Object.keys(document.components.schemas.EmbedAuthorResponse.properties ?? {})).toEqual([
|
||||
'name',
|
||||
'url',
|
||||
'icon_url',
|
||||
'proxy_icon_url',
|
||||
]);
|
||||
});
|
||||
|
||||
it('lists only the fields the admin guild update returns', () => {
|
||||
const document = documentFor('admin');
|
||||
const guild = document.components.schemas.GuildUpdateResponse.properties?.guild;
|
||||
assert(typeof guild === 'object', 'the guild member of GuildUpdateResponse must be an object schema');
|
||||
expect(Object.keys(guild.properties ?? {})).toEqual([
|
||||
'id',
|
||||
'name',
|
||||
'features',
|
||||
'owner_id',
|
||||
'icon',
|
||||
'banner',
|
||||
'member_count',
|
||||
'nsfw_level',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps the report status and type enums in the admin document', () => {
|
||||
const {schemas} = documentFor('admin').components;
|
||||
expect(schemas.ReportStatus).toMatchObject({type: 'integer', format: 'int32', enum: [0, 1]});
|
||||
expect(schemas.ReportType).toMatchObject({type: 'integer', format: 'int32', enum: [0, 1, 2]});
|
||||
});
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
import {ZodOpenAPIConverter} from '@fluxer/openapi/src/converters/ZodToOpenAPI';
|
||||
import type {OpenAPIDocument} from '@fluxer/openapi/src/OpenAPITypes';
|
||||
import {SudoVerificationSchema} from '@fluxer/schema/src/domains/auth/AuthSchemas';
|
||||
import {HarvestArchiveResponse} from '@fluxer/schema/src/domains/user/UserHarvestSchemas';
|
||||
import {createNamedLiteral, SnowflakeType, withOpenApiType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {z} from 'zod';
|
||||
|
||||
function documentFor(converter: ZodOpenAPIConverter): OpenAPIDocument {
|
||||
return {
|
||||
openapi: '3.1.0',
|
||||
info: {title: 'Schema conversion', version: '1'},
|
||||
paths: {},
|
||||
components: {schemas: converter.getAllSchemas(), securitySchemes: {}},
|
||||
};
|
||||
}
|
||||
|
||||
describe('native Zod conversion', () => {
|
||||
it('keeps defaults and nullability separate from input requiredness', () => {
|
||||
const schema = z.object({name: z.string().default('guest'), note: z.string().nullable()});
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
const input = converter.getSchema('Profile', schema, 'input');
|
||||
const output = converter.getSchema('Profile', schema, 'output');
|
||||
expect(input.required).toEqual(['note']);
|
||||
expect(output.required).toEqual(['name', 'note']);
|
||||
expect(input.properties?.name).toMatchObject({type: 'string', default: 'guest'});
|
||||
expect(input.properties?.note).toEqual({type: ['string', 'null']});
|
||||
expect(input.additionalProperties).toBeUndefined();
|
||||
expect(output.additionalProperties).toBe(false);
|
||||
const document = documentFor(converter);
|
||||
converter.normalizeComponents(document);
|
||||
expect(Object.keys(document.components.schemas)).toEqual(['ProfileInput', 'Profile']);
|
||||
});
|
||||
|
||||
it('converts wire input without pretending a bigint transform is a JSON output', () => {
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
expect(converter.getSchema('Snowflake', SnowflakeType, 'input')).toMatchObject({
|
||||
anyOf: [{type: 'string'}, {type: 'integer'}],
|
||||
});
|
||||
expect(() => converter.getSchema('Snowflake', SnowflakeType, 'output')).toThrow(
|
||||
'Could not convert OpenAPI schema Snowflake (output)',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves the shared sudo schema without adding required fields', () => {
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
const schema = converter.getSchema('SudoVerification', SudoVerificationSchema, 'input');
|
||||
expect(schema.type).toBe('object');
|
||||
expect(schema.required).toBeUndefined();
|
||||
expect(schema.properties).toHaveProperty('password');
|
||||
});
|
||||
|
||||
it('renames references without rewriting example payloads or schema property names', () => {
|
||||
const example = {$ref: '#/components/schemas/ChildInput'};
|
||||
const child = z.string().min(1);
|
||||
const schema = z.object({default: child, enum: child}).meta({examples: [example]});
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
converter.register('Child', child);
|
||||
converter.getRef('Parent', schema, 'input');
|
||||
const document = documentFor(converter);
|
||||
converter.normalizeComponents(document);
|
||||
expect(document.components.schemas.Parent.properties).toEqual({
|
||||
default: {$ref: '#/components/schemas/Child'},
|
||||
enum: {$ref: '#/components/schemas/Child'},
|
||||
});
|
||||
expect(document.components.schemas.Parent.examples).toEqual([example]);
|
||||
expect(document.components.schemas.Child).toMatchObject({type: 'string', minLength: 1});
|
||||
expect(document.components.schemas.ChildInput).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps reference descriptions valid for OpenAPI 3.0', () => {
|
||||
const child = z.string().describe('Shared value');
|
||||
const converter = new ZodOpenAPIConverter('openapi-3.0');
|
||||
converter.register('Child', child);
|
||||
converter.getRef('Parent', z.object({child: child.describe('Field value')}), 'input');
|
||||
const document = documentFor(converter);
|
||||
document.openapi = '3.0.3';
|
||||
converter.normalizeComponents(document);
|
||||
expect(document.components.schemas.Parent.properties?.child).toEqual({
|
||||
description: 'Field value',
|
||||
allOf: [{$ref: '#/components/schemas/Child'}],
|
||||
});
|
||||
});
|
||||
|
||||
it('drops the inlined expansion beside a reference for OpenAPI 3.0', () => {
|
||||
const child = z.union([z.string(), z.number()]).describe('Shared value');
|
||||
const converter = new ZodOpenAPIConverter('openapi-3.0');
|
||||
converter.register('Child', child);
|
||||
converter.getRef('Parent', z.object({child: child.describe('Field value')}), 'input');
|
||||
const document = documentFor(converter);
|
||||
document.openapi = '3.0.3';
|
||||
converter.normalizeComponents(document);
|
||||
expect(document.components.schemas.Parent.properties?.child).toEqual({
|
||||
description: 'Field value',
|
||||
allOf: [{$ref: '#/components/schemas/Child'}],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['openapi-3.0', 'draft-2020-12'] as const)('converts binary archive responses for %s', (target) => {
|
||||
const converter = new ZodOpenAPIConverter(target);
|
||||
const schema = converter.getSchema('HarvestArchiveResponse', HarvestArchiveResponse, 'output');
|
||||
expect(schema).toMatchObject({type: 'string', format: 'binary'});
|
||||
expect(schema.contentEncoding).toBe(target === 'openapi-3.0' ? undefined : 'binary');
|
||||
});
|
||||
|
||||
it('rejects different constraints that claim the same component name', () => {
|
||||
const schema = z.object({
|
||||
short: withOpenApiType(z.string().max(10), 'Text'),
|
||||
long: withOpenApiType(z.string().max(100), 'Text'),
|
||||
});
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
expect(() => converter.getSchema('ConflictingText', schema, 'input')).toThrow(
|
||||
'Conflicting OpenAPI schema definitions: Text (input)',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a component name that collides with another schema input name', () => {
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
converter.getRef('Text', z.string(), 'input');
|
||||
expect(() => converter.getRef('TextInput', z.number(), 'output')).toThrow(
|
||||
'Conflicting OpenAPI input/output schema names: TextInput',
|
||||
);
|
||||
});
|
||||
|
||||
it('names discriminated union branches after their discriminator value', () => {
|
||||
const union = z.discriminatedUnion('kind', [
|
||||
z.object({kind: createNamedLiteral(0, 'GUILD_TEXT'), name: z.string()}),
|
||||
z.object({kind: z.literal('refresh_token'), token: z.string()}),
|
||||
z.object({kind: z.literal(7), size: z.number()}),
|
||||
]);
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12', true);
|
||||
converter.getRef('Request', union, 'input');
|
||||
const document = documentFor(converter);
|
||||
converter.normalizeComponents(document);
|
||||
expect(document.components.schemas.Request.oneOf).toEqual([
|
||||
{$ref: '#/components/schemas/GuildTextRequest'},
|
||||
{$ref: '#/components/schemas/RefreshTokenRequest'},
|
||||
{$ref: '#/components/schemas/Variant2Request'},
|
||||
]);
|
||||
expect(document.components.schemas.GuildTextRequest.properties).toHaveProperty('name');
|
||||
expect(document.components.schemas.RefreshTokenRequest.properties).toHaveProperty('token');
|
||||
expect(document.components.schemas.Variant2Request.properties).toHaveProperty('size');
|
||||
});
|
||||
|
||||
it('keeps discriminated union branches inline unless asked to name them', () => {
|
||||
const union = z.discriminatedUnion('kind', [z.object({kind: z.literal('a')}), z.object({kind: z.literal('b')})]);
|
||||
const converter = new ZodOpenAPIConverter('openapi-3.0');
|
||||
const schema = converter.getSchema('Request', union, 'input');
|
||||
expect(schema.oneOf?.map((branch) => branch.$ref)).toEqual([undefined, undefined]);
|
||||
expect(Object.keys(converter.getAllSchemas())).toEqual(['RequestInput']);
|
||||
});
|
||||
|
||||
it('converts newly discovered components after an earlier conversion pass', () => {
|
||||
const converter = new ZodOpenAPIConverter('draft-2020-12');
|
||||
converter.getSchema('First', z.string(), 'input');
|
||||
converter.getRef('Second', z.number(), 'input');
|
||||
expect(converter.getAllSchemas()).toMatchObject({FirstInput: {type: 'string'}, SecondInput: {type: 'number'}});
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {convertPathToOpenAPI, extractPathParameterNames} from '@fluxer/openapi/src/extractors/PathParameters';
|
||||
import {discoverControllerFiles, extractRoutesFromControllers} from '@fluxer/openapi/src/extractors/RouteExtractor';
|
||||
import {OpenAPIOperationBuilder} from '@fluxer/openapi/src/generator/OpenAPIOperationBuilder';
|
||||
import type {ExtractedRoute} from '@fluxer/openapi/src/OpenAPITypes';
|
||||
import {SchemaRegistry} from '@fluxer/openapi/src/registry/SchemaRegistry';
|
||||
import {DonationManageQuery} from '@fluxer/schema/src/domains/donation/DonationSchemas';
|
||||
import {DsaReportEmailSendRequest, OkResponse} from '@fluxer/schema/src/domains/report/ReportSchemas';
|
||||
import {beforeAll, describe, expect, it} from 'vitest';
|
||||
|
||||
const API_PACKAGE_PATH = path.join(fileURLToPath(new URL('../../../../../', import.meta.url)), 'fluxer_api');
|
||||
|
||||
describe('discoverControllerFiles', () => {
|
||||
let files: Array<string>;
|
||||
let routes: Array<ExtractedRoute>;
|
||||
let shapes: Set<string>;
|
||||
beforeAll(() => {
|
||||
files = discoverControllerFiles(API_PACKAGE_PATH);
|
||||
routes = extractRoutesFromControllers(files);
|
||||
shapes = new Set(routes.map((route) => `${route.method.toUpperCase()} ${route.path}`));
|
||||
});
|
||||
it('leaves test sources out of the discovered set', () => {
|
||||
expect(files.filter((file) => file.endsWith('.test.ts'))).toEqual([]);
|
||||
expect(files.filter((file) => file.includes('/tests/'))).toEqual([]);
|
||||
});
|
||||
it('reads routes registered in a *Controller.ts file', () => {
|
||||
expect(shapes).toContain('GET /gifs/search');
|
||||
expect(shapes).toContain('GET /gifs/featured');
|
||||
});
|
||||
it('reads routes registered outside a *Controller.ts file', () => {
|
||||
expect(shapes).toContain('GET /_metrics');
|
||||
expect(shapes).toContain('GET /_health');
|
||||
});
|
||||
it('preserves wire-body overrides separately from runtime validators', () => {
|
||||
const route = routes.find((route) => route.explicitOperationId === 'update_channel');
|
||||
expect(route).toMatchObject({
|
||||
explicitRequestSchemaName: 'ChannelUpdateRequestBody',
|
||||
validators: expect.arrayContaining([{target: 'json', schemaName: 'ChannelUpdateRequest'}]),
|
||||
});
|
||||
});
|
||||
it('preserves an explicit false request-body requirement', () => {
|
||||
const route = routes.find((route) => route.explicitOperationId === 'delete_channel');
|
||||
expect(route).toMatchObject({
|
||||
explicitRequestBodyRequired: false,
|
||||
explicitRequestSchemaName: 'SudoVerificationSchema',
|
||||
});
|
||||
});
|
||||
it('preserves explicitly anonymous security metadata', () => {
|
||||
const route = routes.find((route) => route.path === '/donations/manage');
|
||||
expect(route?.explicitSecurity).toEqual([]);
|
||||
});
|
||||
it('leaves every desktop download redirect undocumented', () => {
|
||||
const downloads = routes.filter((route) => route.path.startsWith('/dl'));
|
||||
expect(downloads.length).toBeGreaterThan(0);
|
||||
for (const route of downloads) {
|
||||
expect(route.explicitOperationId).toBeFalsy();
|
||||
}
|
||||
});
|
||||
it('rejects a bodyless status that is absent from the route response statuses', () => {
|
||||
const route = routes.find((route) => route.path === '/donations/manage');
|
||||
assert(route);
|
||||
const schemaRegistry = new SchemaRegistry();
|
||||
schemaRegistry.registerZod('DonationManageQuery', DonationManageQuery);
|
||||
const builder = new OpenAPIOperationBuilder({schemaRegistry, usedOperationIds: new Set()});
|
||||
expect(() => builder.buildOperation({...route, bodylessStatusCodes: [299]})).toThrow(
|
||||
'Bodyless status 299 is not declared for GET /donations/manage',
|
||||
);
|
||||
});
|
||||
it('documents the extra error statuses a route declares', () => {
|
||||
const route = routes.find((route) => route.explicitOperationId === 'send_dsa_report_email');
|
||||
assert(route);
|
||||
expect(route.errorStatusCodes).toEqual([503]);
|
||||
const schemaRegistry = new SchemaRegistry();
|
||||
schemaRegistry.registerZod('DsaReportEmailSendRequest', DsaReportEmailSendRequest);
|
||||
schemaRegistry.registerZod('OkResponse', OkResponse);
|
||||
const builder = new OpenAPIOperationBuilder({schemaRegistry, usedOperationIds: new Set()});
|
||||
const {responses} = builder.buildOperation(route);
|
||||
expect(Object.keys(responses).sort()).toEqual(['200', '400', '429', '500', '503']);
|
||||
expect(responses['503']).toEqual({
|
||||
description: 'Service Unavailable - The request could not be completed right now',
|
||||
content: {'application/json': {schema: {$ref: '#/components/schemas/Error'}}},
|
||||
});
|
||||
expect(() => builder.buildOperation({...route, explicitOperationId: 'other', errorStatusCodes: [418]})).toThrow(
|
||||
'No documented error response for status 418',
|
||||
);
|
||||
});
|
||||
it('adds no extra error status to a route that declares none', () => {
|
||||
const route = routes.find((route) => route.explicitOperationId === 'verify_dsa_report_email');
|
||||
assert(route);
|
||||
expect(route.errorStatusCodes).toEqual([]);
|
||||
});
|
||||
it('resolves paths and metadata from shared admin route registrations', () => {
|
||||
const route = routes.find((route) => route.explicitOperationId === 'create_admin_blocklist_entry');
|
||||
expect(route).toMatchObject({
|
||||
method: 'post',
|
||||
path: '/admin/blocklists/:list_type/entries',
|
||||
explicitRequestSchemaName: 'AdminBlocklistEntryCreateRequest',
|
||||
});
|
||||
});
|
||||
it('converts route parameters without changing literal path segments', () => {
|
||||
const path = '/admin/voice/regions/:region_id/servers/:server_id';
|
||||
expect(shapes).toContain(`PATCH ${path}`);
|
||||
expect(extractPathParameterNames(path)).toEqual(['region_id', 'server_id']);
|
||||
expect(convertPathToOpenAPI(path)).toBe('/admin/voice/regions/{region_id}/servers/{server_id}');
|
||||
expect(convertPathToOpenAPI('/users/@me')).toBe('/users/@me');
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import {EMPTY_SCOPE, StaticPathResolver, UNRESOLVED} from '@fluxer/openapi/src/extractors/StaticPathResolver';
|
||||
import {Project} from 'ts-morph';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function resolveExpression(expression: string) {
|
||||
const project = new Project({useInMemoryFileSystem: true});
|
||||
const source = project.createSourceFile(
|
||||
'RouteConstants.ts',
|
||||
`const paths = ['/first', '/second']; const path = ${expression};`,
|
||||
);
|
||||
return new StaticPathResolver(project).resolve(
|
||||
source.getVariableDeclarationOrThrow('path').getInitializerOrThrow(),
|
||||
EMPTY_SCOPE,
|
||||
);
|
||||
}
|
||||
|
||||
describe('static route expressions', () => {
|
||||
it.each([0, '0', 1, '1'])('resolves the exact array index %j', (index) => {
|
||||
expect(resolveExpression(`paths[${JSON.stringify(index)}]`)).toBe(Number(index) === 0 ? '/first' : '/second');
|
||||
});
|
||||
|
||||
it.each(['1suffix', '1.5', '01', '1e0', ' 1', '', '-0', -1, 2])(
|
||||
'does not coerce array property %j into a route index',
|
||||
(index) => {
|
||||
expect(resolveExpression(`paths[${JSON.stringify(index)}]`)).toBe(UNRESOLVED);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('imported route constants', () => {
|
||||
function resolveImportedPath(specifier: string) {
|
||||
const project = new Project({useInMemoryFileSystem: true});
|
||||
project
|
||||
.getFileSystem()
|
||||
.writeFileSync('/api/tsconfig.json', JSON.stringify({compilerOptions: {paths: {'@app/*': ['./src/*']}}}));
|
||||
project.createSourceFile('/api/src/constants/Routes.ts', "export const USERS_ROUTE = '/users';");
|
||||
const controller = project.createSourceFile(
|
||||
'/api/src/users/UserController.ts',
|
||||
`import {USERS_ROUTE} from '${specifier}'; const path = USERS_ROUTE;`,
|
||||
);
|
||||
return new StaticPathResolver(project).resolve(
|
||||
controller.getVariableDeclarationOrThrow('path').getInitializerOrThrow(),
|
||||
EMPTY_SCOPE,
|
||||
);
|
||||
}
|
||||
|
||||
it.each(['../constants/Routes', '@app/constants/Routes'])('follows %s to the exported constant', (specifier) => {
|
||||
expect(resolveImportedPath(specifier)).toBe('/users');
|
||||
});
|
||||
|
||||
it('does not follow a bare package import', () => {
|
||||
expect(resolveImportedPath('@fluxer/constants/src/Routes')).toBe(UNRESOLVED);
|
||||
});
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {extractRoutesFromControllers} from '@fluxer/openapi/src/extractors/RouteExtractor';
|
||||
import {OpenAPIOperationBuilder} from '@fluxer/openapi/src/generator/OpenAPIOperationBuilder';
|
||||
import type {OpenAPISchemaTarget} from '@fluxer/openapi/src/OpenAPIGenerationTypes';
|
||||
import type {ExtractedRoute} from '@fluxer/openapi/src/OpenAPITypes';
|
||||
import {SchemaRegistry} from '@fluxer/openapi/src/registry/SchemaRegistry';
|
||||
import {afterAll, beforeAll, describe, expect, it} from 'vitest';
|
||||
|
||||
const CONTROLLER_SOURCE = `
|
||||
export function FixtureController(app: HonoApp) {
|
||||
app.patch(
|
||||
'/fixture/maybe',
|
||||
LoginRequired,
|
||||
OpenAPI({
|
||||
operationId: 'fixture_maybe',
|
||||
summary: 'Fixture',
|
||||
description: 'Fixture route.',
|
||||
responseSchema: FixtureResponse,
|
||||
statusCode: [200, 204],
|
||||
bodylessStatusCodes: [204],
|
||||
tags: ['Fixture'],
|
||||
}),
|
||||
async (ctx) => ctx.body(null, 204),
|
||||
);
|
||||
app.get(
|
||||
'/fixture/always',
|
||||
LoginRequired,
|
||||
OpenAPI({
|
||||
operationId: 'fixture_always',
|
||||
summary: 'Fixture',
|
||||
description: 'Fixture route.',
|
||||
responseSchema: FixtureResponse,
|
||||
tags: ['Fixture'],
|
||||
}),
|
||||
async (ctx) => ctx.json({}),
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
const REF = {$ref: '#/components/schemas/FixtureResponse'};
|
||||
|
||||
describe('operation responses', () => {
|
||||
let directory: string;
|
||||
let routes: Map<string, ExtractedRoute>;
|
||||
beforeAll(() => {
|
||||
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-responses-'));
|
||||
const controllerPath = path.join(directory, 'FixtureController.ts');
|
||||
fs.writeFileSync(controllerPath, CONTROLLER_SOURCE);
|
||||
routes = new Map(extractRoutesFromControllers([controllerPath]).map((route) => [route.path, route]));
|
||||
});
|
||||
afterAll(() => {
|
||||
fs.rmSync(directory, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
function successSchema(routePath: string, target?: OpenAPISchemaTarget) {
|
||||
const route = routes.get(routePath);
|
||||
if (!route) throw new Error(`Missing fixture route ${routePath}`);
|
||||
const schemaRegistry = new SchemaRegistry(target);
|
||||
schemaRegistry.register('FixtureResponse', {type: 'object'});
|
||||
const builder = new OpenAPIOperationBuilder({schemaRegistry, usedOperationIds: new Set()});
|
||||
const responses = builder.buildOperation(route).responses;
|
||||
return {schema: responses['200'].content?.['application/json'].schema, responses};
|
||||
}
|
||||
|
||||
it('marks the body nullable when another success status has no body', () => {
|
||||
const {schema, responses} = successSchema('/fixture/maybe');
|
||||
expect(schema).toEqual({anyOf: [REF, {type: 'null'}]});
|
||||
expect(responses['204']).toEqual({description: 'No Content'});
|
||||
});
|
||||
|
||||
it('uses nullable for the openapi 3.0 target', () => {
|
||||
expect(successSchema('/fixture/maybe', 'openapi-3.0').schema).toEqual({allOf: [REF], nullable: true});
|
||||
});
|
||||
|
||||
it('keeps a plain reference when every success status has a body', () => {
|
||||
expect(successSchema('/fixture/always').schema).toEqual(REF);
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {extractRoutesFromControllers} from '@fluxer/openapi/src/extractors/RouteExtractor';
|
||||
import {OpenAPIOperationBuilder} from '@fluxer/openapi/src/generator/OpenAPIOperationBuilder';
|
||||
import type {ExtractedRoute} from '@fluxer/openapi/src/OpenAPITypes';
|
||||
import {SchemaRegistry} from '@fluxer/openapi/src/registry/SchemaRegistry';
|
||||
import {afterAll, beforeAll, describe, expect, it} from 'vitest';
|
||||
|
||||
const CONTROLLER_SOURCE = `
|
||||
export function FixtureController(app: HonoApp) {
|
||||
app.get(
|
||||
'/fixture/bot',
|
||||
LoginRequired,
|
||||
BotOnly,
|
||||
OpenAPI({
|
||||
operationId: 'fixture_bot',
|
||||
summary: 'Fixture',
|
||||
description: 'Fixture route.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
tags: ['Fixture'],
|
||||
}),
|
||||
async (ctx) => ctx.body(null, 204),
|
||||
);
|
||||
app.get(
|
||||
'/fixture/user',
|
||||
LoginRequired,
|
||||
DefaultUserOnly,
|
||||
OpenAPI({
|
||||
operationId: 'fixture_user',
|
||||
summary: 'Fixture',
|
||||
description: 'Fixture route.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
tags: ['Fixture'],
|
||||
}),
|
||||
async (ctx) => ctx.body(null, 204),
|
||||
);
|
||||
app.get(
|
||||
'/fixture/any',
|
||||
LoginRequired,
|
||||
OpenAPI({
|
||||
operationId: 'fixture_any',
|
||||
summary: 'Fixture',
|
||||
description: 'Fixture route.',
|
||||
responseSchema: null,
|
||||
statusCode: 204,
|
||||
tags: ['Fixture'],
|
||||
}),
|
||||
async (ctx) => ctx.body(null, 204),
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
describe('operation security', () => {
|
||||
let directory: string;
|
||||
let routes: Map<string, ExtractedRoute>;
|
||||
beforeAll(() => {
|
||||
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-security-'));
|
||||
const controllerPath = path.join(directory, 'FixtureController.ts');
|
||||
fs.writeFileSync(controllerPath, CONTROLLER_SOURCE);
|
||||
routes = new Map(extractRoutesFromControllers([controllerPath]).map((route) => [route.path, route]));
|
||||
});
|
||||
afterAll(() => {
|
||||
fs.rmSync(directory, {recursive: true, force: true});
|
||||
});
|
||||
|
||||
function securityOf(routePath: string) {
|
||||
const route = routes.get(routePath);
|
||||
if (!route) throw new Error(`Missing fixture route ${routePath}`);
|
||||
const builder = new OpenAPIOperationBuilder({schemaRegistry: new SchemaRegistry(), usedOperationIds: new Set()});
|
||||
return {route, security: builder.buildOperation(route).security};
|
||||
}
|
||||
|
||||
it('declares bot token security for a BotOnly route', () => {
|
||||
const {route, security} = securityOf('/fixture/bot');
|
||||
expect(route.hasBotOnly).toBe(true);
|
||||
expect(security).toEqual([{botToken: []}]);
|
||||
});
|
||||
|
||||
it('keeps DefaultUserOnly routes on user tokens', () => {
|
||||
const {route, security} = securityOf('/fixture/user');
|
||||
expect(route.hasBotOnly).toBe(false);
|
||||
expect(route.hasDefaultUserOnly).toBe(true);
|
||||
expect(security).toEqual([{sessionToken: []}]);
|
||||
});
|
||||
|
||||
it('keeps LoginRequired routes open to bot and user tokens', () => {
|
||||
const {route, security} = securityOf('/fixture/any');
|
||||
expect(route.hasBotOnly).toBe(false);
|
||||
expect(security).toEqual([{botToken: []}, {sessionToken: []}]);
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import {SchemaRegistry} from '@fluxer/openapi/src/registry/SchemaRegistry';
|
||||
import {
|
||||
CreateVoiceServerRequestBody,
|
||||
UpdateVoiceRegionRequest,
|
||||
UpdateVoiceRegionRequestBody,
|
||||
} from '@fluxer/schema/src/domains/admin/AdminVoiceSchemas';
|
||||
import {SudoVerificationSchema} from '@fluxer/schema/src/domains/auth/AuthSchemas';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {z} from 'zod';
|
||||
|
||||
describe('request schema registry', () => {
|
||||
it('checks the empty object supplied by the JSON body reader', () => {
|
||||
const registry = new SchemaRegistry();
|
||||
registry.registerZod('SudoVerification', SudoVerificationSchema);
|
||||
registry.registerZod('UpdateVoiceRegion', UpdateVoiceRegionRequest);
|
||||
registry.registerZod('UpdateVoiceRegionBody', UpdateVoiceRegionRequestBody);
|
||||
registry.registerZod('CreateVoiceServerBody', CreateVoiceServerRequestBody);
|
||||
expect(registry.acceptsEmptyObject('SudoVerification')).toBe(true);
|
||||
expect(registry.acceptsEmptyObject('UpdateVoiceRegion')).toBe(false);
|
||||
expect(registry.acceptsEmptyObject('UpdateVoiceRegionBody')).toBe(true);
|
||||
expect(registry.acceptsEmptyObject('CreateVoiceServerBody')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not treat optional properties as proof that an empty body is valid', () => {
|
||||
const registry = new SchemaRegistry();
|
||||
registry.registerZod(
|
||||
'Edit',
|
||||
z.object({name: z.string().optional()}).refine((value) => value.name !== undefined),
|
||||
);
|
||||
expect(registry.acceptsEmptyObject('Edit')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing schemas and overlapping static definitions', () => {
|
||||
const registry = new SchemaRegistry();
|
||||
expect(() => registry.getRef('Missing')).toThrow('Unknown OpenAPI schema: Missing');
|
||||
registry.register('Text', {type: 'string'});
|
||||
expect(() => registry.registerZod('Text', z.string())).toThrow('Zod schema also has an OpenAPI definition: Text');
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {defineConfig} from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {tsconfigPaths: true},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
exclude: ['node_modules', 'dist'],
|
||||
testTimeout: 60000,
|
||||
hookTimeout: 60000,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.test.tsx', '**/*.spec.tsx', 'node_modules/'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user