1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

chore: test requirements for openapi (#3511)

## About the changes
This enables us to validate the shape of our OpenAPI schemas by defining
specific json-schema rules that will be evaluated against all our open
API schemas.

Because we know there are things we need to improve, we've added a list
of `knownExceptions`. When fixing some of the known exceptions the tests
will force us to remove the exception from the list, that way
contributing to reducing the number of violations to our own rules.

Co-authored-by: Mateusz Kwasniewski <kwasniewski.mateusz@gmail.com>
Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
Gastón Fournier 2023-04-14 10:37:17 +02:00 committed by GitHub
parent 39b53c8f2c
commit 19982ecbc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 232 additions and 23 deletions

View File

@ -17,15 +17,6 @@ test('all schema files should be added to the schemas object', () => {
expect(expectedSchemaNames.sort()).toEqual(addedSchemaNames.sort());
});
test('all schema $id attributes should have the expected format', () => {
const schemaIds = Object.values(schemas).map((schema) => schema.$id);
const schemaIdRegExp = new RegExp(`^#/components/schemas/[a-z][a-zA-Z]+$`);
schemaIds.forEach((schemaId) => {
expect(schemaId).toMatch(schemaIdRegExp);
});
});
test('removeJsonSchemaProps', () => {
expect(removeJsonSchemaProps({ a: 'b', $id: 'c', components: {} }))
.toMatchInlineSnapshot(`

View File

@ -147,8 +147,36 @@ import { clientMetricsEnvSchema } from './spec/client-metrics-env-schema';
import { updateTagsSchema } from './spec/update-tags-schema';
import { batchStaleSchema } from './spec/batch-stale-schema';
// Schemas must have an $id property on the form "#/components/schemas/mySchema".
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
// Schemas must list all their $refs in `components`, including nested schemas.
export type SchemaRef = typeof schemas[keyof typeof schemas]['components'];
// JSON schema properties that should not be included in the OpenAPI spec.
export interface JsonSchemaProps {
$id: string;
components: object;
}
type SchemaWithMandatoryFields = Partial<
Omit<
OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject,
'$id' | 'components'
>
> &
JsonSchemaProps;
interface UnleashSchemas {
[name: string]: SchemaWithMandatoryFields;
}
interface OpenAPIV3DocumentWithServers extends OpenAPIV3.Document {
servers: OpenAPIV3.ServerObject[];
}
// All schemas in `openapi/spec` should be listed here.
export const schemas = {
export const schemas: UnleashSchemas = {
adminFeaturesQuerySchema,
addonParameterSchema,
addonSchema,
@ -291,18 +319,6 @@ export const schemas = {
importTogglesValidateItemSchema,
};
// Schemas must have an $id property on the form "#/components/schemas/mySchema".
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
// Schemas must list all their $refs in `components`, including nested schemas.
export type SchemaRef = typeof schemas[keyof typeof schemas]['components'];
// JSON schema properties that should not be included in the OpenAPI spec.
export interface JsonSchemaProps {
$id: string;
components: object;
}
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
export const removeJsonSchemaProps = <T extends JsonSchemaProps>(
schema: T,
@ -328,7 +344,7 @@ export const createOpenApiSchema = ({
unleashUrl,
baseUriPath,
}: Pick<IServerOption, 'unleashUrl' | 'baseUriPath'>): Omit<
OpenAPIV3.Document,
OpenAPIV3DocumentWithServers,
'paths'
> => {
const url = findRootUrl(unleashUrl, baseUriPath);

View File

@ -0,0 +1,202 @@
import Ajv, { Schema } from 'ajv';
import { schemas } from '.';
const ajv = new Ajv();
type SchemaNames = keyof typeof schemas;
type Rule = {
name: string;
match?: (
schemaName: string,
schema: typeof schemas[SchemaNames],
) => boolean;
metaSchema: Schema;
knownExceptions?: string[];
};
const metaRules: Rule[] = [
{
name: 'should have a type',
metaSchema: {
type: 'object',
properties: {
type: { type: 'string', enum: ['object', 'array'] },
},
required: ['type'],
},
knownExceptions: ['dateSchema'],
},
{
name: 'should have an $id with the expected format',
metaSchema: {
type: 'object',
properties: {
$id: {
type: 'string',
pattern: '^#/components/schemas/[a-z][a-zA-Z]+$',
},
},
required: ['$id'],
},
},
{
name: 'should have a description',
metaSchema: {
type: 'object',
properties: {
description: { type: 'string' },
},
required: ['description'],
},
knownExceptions: [
'adminFeaturesQuerySchema',
'addonParameterSchema',
'addonSchema',
'addonsSchema',
'addonTypeSchema',
'apiTokenSchema',
'apiTokensSchema',
'applicationSchema',
'applicationsSchema',
'batchFeaturesSchema',
'batchStaleSchema',
'bulkRegistrationSchema',
'bulkMetricsSchema',
'changePasswordSchema',
'clientApplicationSchema',
'clientFeatureSchema',
'clientFeaturesQuerySchema',
'clientFeaturesSchema',
'clientMetricsSchema',
'clientMetricsEnvSchema',
'cloneFeatureSchema',
'contextFieldSchema',
'contextFieldsSchema',
'createApiTokenSchema',
'createFeatureSchema',
'createFeatureStrategySchema',
'createInvitedUserSchema',
'createUserSchema',
'dateSchema',
'edgeTokenSchema',
'emailSchema',
'environmentsSchema',
'eventSchema',
'eventsSchema',
'exportResultSchema',
'exportQuerySchema',
'featureEnvironmentMetricsSchema',
'featureEventsSchema',
'featureMetricsSchema',
'featureSchema',
'featuresSchema',
'featureStrategySchema',
'featureStrategySegmentSchema',
'featureTagSchema',
'featureTypeSchema',
'featureTypesSchema',
'featureUsageSchema',
'featureVariantsSchema',
'feedbackSchema',
'groupSchema',
'groupsSchema',
'groupUserModelSchema',
'healthCheckSchema',
'healthOverviewSchema',
'healthReportSchema',
'idSchema',
'instanceAdminStatsSchema',
'legalValueSchema',
'loginSchema',
'maintenanceSchema',
'toggleMaintenanceSchema',
'meSchema',
'nameSchema',
'overrideSchema',
'parametersSchema',
'passwordSchema',
'patchesSchema',
'patchSchema',
'patSchema',
'patsSchema',
'permissionSchema',
'playgroundSegmentSchema',
'playgroundStrategySchema',
'profileSchema',
'projectEnvironmentSchema',
'proxyClientSchema',
'proxyFeatureSchema',
'proxyFeaturesSchema',
'publicSignupTokenCreateSchema',
'publicSignupTokenSchema',
'publicSignupTokensSchema',
'publicSignupTokenUpdateSchema',
'pushVariantsSchema',
'resetPasswordSchema',
'requestsPerSecondSchema',
'requestsPerSecondSegmentedSchema',
'roleSchema',
'segmentSchema',
'setStrategySortOrderSchema',
'setUiConfigSchema',
'sortOrderSchema',
'splashSchema',
'stateSchema',
'strategiesSchema',
'strategySchema',
'tagsBulkAddSchema',
'tagSchema',
'tagsSchema',
'tagTypeSchema',
'tagTypesSchema',
'tagWithVersionSchema',
'tokenUserSchema',
'uiConfigSchema',
'updateApiTokenSchema',
'updateFeatureSchema',
'updateFeatureStrategySchema',
'updateTagTypeSchema',
'updateUserSchema',
'updateTagsSchema',
'upsertContextFieldSchema',
'upsertSegmentSchema',
'upsertStrategySchema',
'userSchema',
'usersGroupsBaseSchema',
'usersSchema',
'usersSearchSchema',
'validateEdgeTokensSchema',
'validatePasswordSchema',
'validateTagTypeSchema',
'variantSchema',
'variantsSchema',
'versionSchema',
'importTogglesSchema',
'importTogglesValidateSchema',
'importTogglesValidateItemSchema',
],
},
];
describe.each(metaRules)('OpenAPI schemas $name', (rule) => {
const validateMetaSchema = ajv.compile(rule.metaSchema);
// test all schemas agaisnt the rule
Object.entries(schemas).forEach(([schemaName, schema]) => {
if (!rule.match || rule.match(schemaName, schema)) {
it(`${schemaName}`, () => {
validateMetaSchema(schema);
// note: whenever you resolve an exception please remove it from the list
if (rule.knownExceptions?.includes(schemaName)) {
console.warn(
`${schemaName} is a known exception to rule "${rule.name}" that should be fixed`,
);
expect(validateMetaSchema.errors).not.toBeNull();
} else {
expect(validateMetaSchema.errors).toBeNull();
}
});
}
});
});