1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00
unleash.unleash/src/test/e2e/api/openapi/openapi.e2e.test.ts
Thomas Heartman b65bfaa52f
chore: remove OpenAPI snapshot tests (#4153)
Background (general): I don’t think it creates any value. I don’t think
people check the snapshot explicitly (especially now that it’s so large
that it gets hidden by default), and it only creates an extra chore that
you forget to do all the time (at least I do).

Background (why now?): I’ve been fighting a weird issue with the
snapshot test for the [API tokens OpenAPI
PR](https://github.com/Unleash/unleash/pull/4137). The tests run fine
locally, but fail in CI. The tests seem to be using an old version of
the snapshot for the test. However, when I check the snapshot in the
files tab, it’s got the new and correct values. I’ve spent probably more
than an hour trying to fix and debug this today but to no avail.
2023-07-06 10:16:27 +03:00

189 lines
6.7 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);
}
expect(enforcerWarning ?? enforcerError).toBe(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({});
});