ibm-openapi-validator
Version:
Configurable and extensible validator/linter for OpenAPI documents
245 lines (195 loc) • 8.53 kB
JavaScript
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { readFile } = require('node:fs/promises');
const readYaml = require('js-yaml');
const { collections } = require('@ibm-cloud/openapi-ruleset-utilities');
const { Metrics } = require('../../src/scoring-tool/metrics');
describe('scoring-tool metrics tests', function () {
let apiDef;
beforeAll(async function () {
const fileToTest = `${__dirname}/../cli-validator/mock-files/oas3/clean.yml`;
const contents = await readFile(fileToTest, { encoding: 'utf8' });
apiDef = readYaml.load(contents);
});
it('should initialize members in constructor', function () {
const metrics = new Metrics(apiDef);
expect(metrics.apiDefinition).toEqual(apiDef);
expect(metrics.callbacks).toEqual({});
expect(metrics.counts).toEqual({});
expect(metrics.collectedArtifacts).toEqual({});
});
it('should register a basic callback that increments the count', function () {
const metricName = 'paths';
const jsonPaths = collections.paths;
const metrics = new Metrics(apiDef);
const countEveryInstance = jest.fn(() => true);
const input = { value: '', path: '' };
metrics.register(metricName, jsonPaths, countEveryInstance);
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName]).toEqual(new Set());
jsonPaths.forEach(jp => {
expect(metrics.callbacks[jp]).toBeInstanceOf(Array);
metrics.callbacks[jp].forEach(cb => cb(input));
});
expect(countEveryInstance).toHaveBeenCalledTimes(jsonPaths.length);
expect(metrics.counts[metricName]).toBe(jsonPaths.length);
});
it('should registerSchemas - recursive callback that increments the count', function () {
const metricName = 'paths';
const jsonPaths = collections.paths;
const metrics = new Metrics(apiDef);
const countEveryInstance = jest.fn(() => true);
const input = {
value: {
type: 'object',
properties: {
name: {
type: 'string',
},
},
},
path: ['components', 'schemas', 'Model'],
};
expect(metrics.counts[metricName]).toBeUndefined();
expect(metrics.collectedArtifacts[metricName]).toBeUndefined();
metrics.registerSchemas(metricName, jsonPaths, countEveryInstance);
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName]).toEqual(new Set());
jsonPaths.forEach(jp => {
expect(metrics.callbacks[jp]).toBeInstanceOf(Array);
metrics.callbacks[jp].forEach(cb => cb(input));
});
// Everything should be called twice, since two schemas will be reported on:
// the parent and the nested schema.
const timesCalled = jsonPaths.length * 2;
expect(countEveryInstance).toHaveBeenCalledTimes(timesCalled);
expect(metrics.counts[metricName]).toBe(timesCalled);
});
it('should compute - populate the metrics', async function () {
const metricName = 'paths';
const jsonPaths = collections.paths;
const metrics = new Metrics(apiDef);
const countEveryInstance = jest.fn(() => true);
expect(metrics.counts).toEqual({});
expect(metrics.collectedArtifacts).toEqual({});
// Set up a basic callback.
metrics.register(metricName, jsonPaths, countEveryInstance);
await metrics.compute();
expect(metrics.counts[metricName]).toBe(2);
expect(metrics.collectedArtifacts[metricName].size).toBe(2);
// Check that the callback received the API paths.
expect(countEveryInstance.mock.calls.map(c => c[1])).toEqual([
['paths', '/pets'],
['paths', '/pets/{pet_id}'],
]);
});
it('should get the count for a given metric', function () {
const metrics = new Metrics(apiDef);
metrics.counts = { paths: 4 };
expect(metrics.get('paths')).toBe(4);
expect(metrics.get('other')).toBeUndefined();
});
it('should toString', function () {
const metrics = new Metrics(apiDef);
const counts = { paths: 4 };
metrics.counts = counts;
expect(metrics.toString()).toBe(JSON.stringify(counts, null, 2));
});
it('should registerCallback - baseline callback registration', function () {
const metrics = new Metrics(apiDef);
const metricName = 'paths';
const jsonPaths = collections.paths;
const countEveryInstance = jest.fn(() => true);
const input = { value: '', path: '' };
expect(metrics.counts[metricName]).toBeUndefined();
expect(metrics.collectedArtifacts[metricName]).toBeUndefined();
metrics.registerCallback(metricName, jsonPaths, countEveryInstance);
// Should initialize the metric values.
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName]).toEqual(new Set());
// Should store a callback for each JSON path.
jsonPaths.forEach(jp => {
expect(metrics.callbacks[jp]).toBeInstanceOf(Array);
metrics.callbacks[jp].forEach(cb => cb(input));
});
expect(countEveryInstance).toHaveBeenCalledTimes(jsonPaths.length);
});
it('should increment and store value', function () {
const metrics = new Metrics(apiDef);
const metricName = 'paths';
const mockPath = '/v1/my-path';
const mockPathObject = { get: 'my-operation' };
const mockCondition = jest.fn(() => true);
// These would be initialized by a different method, fake it here.
metrics.initializeMetric(metricName);
// Expect there to be no count or storage at the beginning.
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName].has(mockPathObject)).toBe(
false
);
// Run the increment method.
metrics.increment(mockPathObject, mockPath, metricName, mockCondition);
expect(mockCondition).toHaveBeenCalledWith(mockPathObject, mockPath);
expect(metrics.counts[metricName]).toBe(1);
expect(metrics.collectedArtifacts[metricName].has(mockPathObject)).toBe(
true
);
});
it('should not increment if condition returns false', function () {
const metrics = new Metrics(apiDef);
const metricName = 'paths';
const mockPath = '/v1/my-path';
const mockPathObject = { get: 'my-operation' };
const mockCondition = jest.fn(() => false);
// These would be initialized by a different method, fake it here.
metrics.initializeMetric(metricName);
// Expect there to be no count or storage at the beginning.
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName].has(mockPathObject)).toBe(
false
);
// Run the increment method.
metrics.increment(mockPathObject, mockPath, metricName, mockCondition);
expect(mockCondition).toHaveBeenCalledWith(mockPathObject, mockPath);
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName].has(mockPathObject)).toBe(
true
);
});
it('should not increment if artifact has already been collected', function () {
const metrics = new Metrics(apiDef);
const metricName = 'paths';
const mockPath = '/v1/my-path';
const mockPathObject = { get: 'my-operation' };
const mockCondition = jest.fn(() => true);
// These would be initialized by a different method, fake it here.
metrics.initializeMetric(metricName);
// Make it look like we've already visited this artifact.
metrics.collectedArtifacts[metricName].add(mockPathObject);
// Expect there to be no count but the object already stored at the beginning.
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName].has(mockPathObject)).toBe(
true
);
// Run the increment method.
metrics.increment(mockPathObject, mockPath, metricName, mockCondition);
expect(mockCondition).toHaveBeenCalledWith(mockPathObject, mockPath);
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName].has(mockPathObject)).toBe(
true
);
});
it('should initializeMetric', function () {
const metrics = new Metrics(apiDef);
const metricName = 'paths';
// Should start out undefined.
expect(metrics.counts[metricName]).toBeUndefined();
expect(metrics.collectedArtifacts[metricName]).toBeUndefined();
metrics.initializeMetric(metricName);
// Should be initialized to the baseline values.
expect(metrics.counts[metricName]).toBe(0);
expect(metrics.collectedArtifacts[metricName]).toEqual(new Set());
});
});