mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
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.
189 lines
6.7 KiB
TypeScript
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({});
|
|
});
|