1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00
unleash.unleash/src/test/e2e/api/openapi/openapi.e2e.test.ts
Christopher Kolstad 6673d131fe
feat: biome lint (#4853)
This commit changes our linter/formatter to biome (https://biomejs.dev/)
Causing our prehook to run almost instantly, and our "yarn lint" task to
run in sub 100ms.

Some trade-offs:
* Biome isn't quite as well established as ESLint
* Are we ready to install a different vscode plugin (the biome plugin)
instead of the prettier plugin


The configuration set for biome also has a set of recommended rules,
this is turned on by default, in order to get to something that was
mergeable I have turned off a couple the rules we seemed to violate the
most, that we also explicitly told eslint to ignore.
2023-09-29 14:18:21 +02:00

224 lines
7.9 KiB
TypeScript

import { setupApp } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import SwaggerParser from '@apidevtools/swagger-parser';
import enforcer from 'openapi-enforcer';
import semver from 'semver';
import { openApiTags } from '../../../../lib/openapi/util/openapi-tags';
let app;
let db;
beforeAll(async () => {
db = await dbInit('openapi', getLogger);
app = await setupApp(db.stores);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('should serve the OpenAPI UI', async () => {
return app.request
.get('/docs/openapi/')
.expect('Content-Type', /html/)
.expect(200);
});
test('should serve the OpenAPI spec', async () => {
return app.request
.get('/docs/openapi.json')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
// Don't use the version field in snapshot tests. Having the version
// listed in automated testing causes issues when trying to deploy
// new versions of the API (due to mismatch between new tag versions etc).
delete res.body.info.version;
});
});
test('should serve the OpenAPI spec with a `version` property', async () => {
return app.request
.get('/docs/openapi.json')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
const { version } = res.body.info;
// ensure there's no whitespace or leading `v`
expect(semver.clean(version)).toStrictEqual(version);
// ensure the version listed is valid semver
expect(semver.parse(version, { loose: false })).toBeTruthy();
});
});
test('the generated OpenAPI spec is valid', async () => {
const { body } = await app.request
.get('/docs/openapi.json')
.expect('Content-Type', /json/)
.expect(200);
// this throws if the swagger parser can't parse it correctly
// also parses examples, but _does_ do some string coercion in examples
try {
await SwaggerParser.validate(body);
} catch (err) {
console.error(err);
// there's an error here, so let's exit after showing it in the console.
expect(true).toBe(false);
}
const [, enforcerError, enforcerWarning] = await enforcer(body, {
fullResult: true,
componentOptions: {
exceptionSkipCodes: [
// allow non-standard formats for strings (including 'uri')
'WSCH001',
// Schemas with an indeterminable type cannot serialize,
// deserialize, or validate values. [WSCH005]
//
// This allows specifying the 'any' type for schemas (such as the
// patchSchema)
'WSCH005',
],
},
});
if (enforcerWarning !== undefined) {
console.warn(enforcerWarning);
}
if (enforcerError !== undefined) {
console.error(enforcerError);
}
const enforcerResults = {
warnings: enforcerWarning?.toString(),
errors: enforcerError?.toString(),
};
expect(enforcerResults).toMatchObject({
warnings: undefined,
errors: undefined,
});
});
test('all root-level tags are "approved tags"', async () => {
const { body: spec } = await app.request
.get('/docs/openapi.json')
.expect('Content-Type', /json/)
.expect(200);
const specTags = spec.tags;
const approvedTags = openApiTags;
// expect spec tags to be a subset of the approved tags
expect(approvedTags).toEqual(expect.arrayContaining(specTags));
});
// All tags that are used for OpenAPI path operations must also be listed in the
// OpenAPI root-level tags list. For us, there's two immediate things that make
// this important:
//
// 1. Swagger UI groups operations by tags. To make sure that endpoints are
// listed where users would expect to find them, they should be given an
// appropriate tag.
//
// 2. The OpenAPI/docusaurus integration we use does not generate documentation
// for paths whose tags are not listed in the root-level tags list.
//
// If none of the official tags seem appropriate for an endpoint, consider
// creating a new tag.
test('all tags are listed in the root "tags" list', async () => {
const { body: spec } = await app.request
.get('/docs/openapi.json')
.expect('Content-Type', /json/)
.expect(200);
const rootLevelTagNames = new Set(spec.tags.map((tag) => tag.name));
// dictionary of all invalid tags found in the spec
let invalidTags = {};
for (const [path, data] of Object.entries(spec.paths)) {
for (const [operation, opData] of Object.entries(data)) {
// ensure that the list of tags for every operation is a subset of
// the list of tags defined on the root level
// check each tag for this operation
for (const tag of opData.tags) {
if (!rootLevelTagNames.has(tag)) {
// store other invalid tags that already exist on this
// operation
const preExistingTags =
invalidTags[path]?.[operation]?.invalidTags ?? [];
// add information about the invalid tag to the invalid tags
// dict.
invalidTags = {
...invalidTags,
[path]: {
...invalidTags[path],
[operation]: {
operationId: opData.operationId,
invalidTags: [...preExistingTags, tag],
},
},
};
}
}
}
}
if (Object.keys(invalidTags).length) {
// create a human-readable list of invalid tags per operation
const msgs = Object.entries(invalidTags).flatMap(([path, data]) =>
Object.entries(data).map(
([operation, opData]) =>
`${operation.toUpperCase()} ${path} (operation id: ${
opData.operationId
}) has the following invalid tags: ${opData.invalidTags
.map((tag) => `"${tag}"`)
.join(', ')}`,
),
);
// format message
const errorMessage = `The OpenAPI spec contains path-level tags that are not listed in the root-level tags object. The relevant paths, operation ids, and tags are as follows:\n\n${msgs.join(
'\n\n',
)}\n\nFor reference, the root-level tags are: ${spec.tags
.map((tag) => `"${tag.name}"`)
.join(', ')}`;
console.error(errorMessage);
}
expect(invalidTags).toStrictEqual({});
});
test('all API operations have non-empty summaries and descriptions', async () => {
const { body: spec } = await app.request
.get('/docs/openapi.json')
.expect('Content-Type', /json/)
.expect(200);
const anomalies = Object.entries(spec.paths).flatMap(([path, data]) => {
return Object.entries(data)
.map(([verb, operationDescription]) => {
if (
operationDescription.summary &&
operationDescription.description
) {
return undefined;
} else {
return [verb, operationDescription.operationId];
}
})
.filter(Boolean)
.map(
([verb, operationId]) =>
`${verb.toUpperCase()} ${path} (operation ID: ${operationId})`,
);
});
// any items left in the anomalies list is missing either a summary, or a
// description, or both.
expect(anomalies).toStrictEqual([]);
});