@apistudio/apim-cli
Version:
CLI for API Management Products
470 lines (424 loc) • 15.3 kB
text/typescript
/**
* Copyright IBM Corp. 2024, 2025
*/
import { VCM } from '../../src/engine/variable-context-manager/context-manager.js';
import { ModelFactory } from '../../src/model-factories/model.factory.js';
import { Test } from '../../src/schemas/test.schema.js';
import {
validAssertionData,
validAssertionDataV2,
} from '../__mocks__/test-data/assertion.data.js';
import {
validEnvironmentData,
validEnvironmentDataV2,
} from '../__mocks__/test-data/environment.data.js';
import {
gatewayEnv,
multiGatewayAPIEnv,
multiGatewayEnv,
} from '../__mocks__/test-data/gateway.data.js';
import {
invalidTestSchema,
testWithEmptyEnvironment,
testWithEmptyAssertion,
unsupportedKind,
validTest,
validTests,
validTestWithEnvironmentsAndAssertions,
validTestWithMultiAPIAndEnvironment,
} from '../__mocks__/test-data/test.data.js';
describe('ModelFactory', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should validate session and undefined kind', () => {
const env = {
'PaymentAPI:1.0.1': ['https://localhost:3000/'],
};
const factory = new ModelFactory();
factory.create([env]);
expect(factory.getGateway()).toEqual(env);
expect(factory.getAllTests()).toHaveLength(0);
factory.destroy();
expect(factory.getAllTests()).toHaveLength(0);
});
it('should store and return valid test', () => {
const factory = new ModelFactory();
factory.create([validTest]);
const allTests = factory.getAllTests();
expect(allTests).toHaveLength(1);
expect(allTests[0].metadata!.name).toBe('TestPayments');
expect(factory.getAllAssertions()).toHaveLength(0);
expect(factory.getAllEnvironment()).toHaveLength(0);
const {
metadata: { namespace, name, version },
} = validTest;
const result = factory.getTest(namespace, name, version);
expect(result).toBeDefined();
expect(result?.metadata?.name).toBe(name);
});
it('should store and return valid assertions', () => {
const factory = new ModelFactory();
factory.create([validAssertionData]);
const allTests = factory.getAllTests();
expect(allTests).toHaveLength(0);
expect(factory.getAllAssertions()).toHaveLength(1);
expect(factory.getAllEnvironment()).toHaveLength(0);
const {
metadata: { namespace, name, version },
} = validAssertionData;
const result = factory.getAssertions(namespace, name, version);
expect(result).toBeDefined();
expect(result?.metadata?.name).toBe(name);
});
it('should store and return valid environment', () => {
const factory = new ModelFactory();
factory.create([validEnvironmentData]);
const allTests = factory.getAllTests();
expect(allTests).toHaveLength(0);
expect(factory.getAllAssertions()).toHaveLength(0);
expect(factory.getAllEnvironment()).toHaveLength(1);
const {
metadata: { namespace, name, version },
} = validEnvironmentData;
const result = factory.getEnvironment(namespace, name, version);
expect(result).toBeDefined();
expect(result?.metadata?.name).toBe(name);
});
it('should throw for invalid schema', () => {
const factory = new ModelFactory();
expect(() => factory.create([invalidTestSchema])).toThrow();
});
it('should ignore or throw error for unsupported kind', () => {
const factory = new ModelFactory();
expect(() => factory.create([unsupportedKind])).toThrow(); // Assuming you silently ignore
expect(factory.getAllTests()).toHaveLength(0);
expect(factory.getAllAssertions()).toHaveLength(0);
expect(factory.getAllEnvironment()).toHaveLength(0);
});
});
describe('ModelFactory.resolveRefs', () => {
let factory: ModelFactory;
beforeEach(() => {
factory = new ModelFactory();
});
it('should resolve API, environment, and assertions for each test ', () => {
factory.create(gatewayEnv);
factory.create([validAssertionData]);
factory.create([validEnvironmentData]);
factory.create([validTest]);
factory.resolveRefs();
const getTest = factory.getAllTests();
expect(getTest).toHaveLength(1);
// Get the environment data to check whether same data is modelled.
const env = factory.getEnvironment(
'default',
'TestPaymentsEnvironment',
'1.0.0',
);
env?.spec?.variables!.forEach((variable) => {
const resolvedEnvironment = getTest[0].spec.environment as {
variables: Array<{
spec: { variables: any[] };
}>;
};
expect(resolvedEnvironment.variables[0].spec.variables).toContainEqual(
variable,
);
expect(
VCM.createContext(getTest[0].vcmId!).get(variable.key!),
).toMatchObject({
value: variable.value,
});
});
// Get the assertion data to check whether same data is modelled.
const assertions = factory.getAssertions(
'default',
'TestPaymentAssertion',
'1.0.0',
);
assertions?.spec!.forEach((assert) => {
const requestAssertions = getTest[0].spec.request[0].assertions;
const resolvedAssertions =
requestAssertions && 'assertions' in requestAssertions
? requestAssertions.assertions
: [];
expect(resolvedAssertions).toBeDefined();
if (resolvedAssertions) {
const allAssertionSpecs = resolvedAssertions.flatMap((a) => a.spec);
expect(allAssertionSpecs).toContainEqual(assert);
}
});
});
it('should resolve API, environment, and assertions for each multi gateway and multiple environment test ', () => {
factory.create(gatewayEnv);
factory.create([validAssertionData, validAssertionDataV2]);
factory.create([validEnvironmentData, validEnvironmentDataV2]);
factory.create([validTestWithEnvironmentsAndAssertions]);
factory.resolveRefs();
const getTests = factory.getAllTests();
expect(getTests).toHaveLength(2);
expect(getTests[0].vcmId).not.toEqual(getTests[1].vcmId);
});
it('should resolve 2 endpoints with 1 environment ', () => {
factory.create(multiGatewayEnv);
factory.create([validAssertionData]);
factory.create([validEnvironmentData]);
factory.create([validTest]);
factory.create([validEnvironmentDataV2]);
factory.resolveRefs();
const getTests = factory.getAllTests();
expect(getTests).toHaveLength(2);
});
it('should resolve 2 gateway endpoints with 2 test suite', () => {
factory.create(multiGatewayEnv);
factory.create([validAssertionData]);
factory.create([validEnvironmentData]);
const patchedValidTests = validTests.map((t) => ({
...t,
spec: {
...t.spec,
environment: { $ref: 'default:TestPaymentsEnvironment:1.0.0' },
},
}));
factory.create(patchedValidTests);
factory.create([validEnvironmentDataV2]);
factory.resolveRefs();
const getTests = factory.getAllTests();
expect(getTests).toHaveLength(4);
});
it('should resolve 1 endpoint and 2 environments ', () => {
factory.create([validAssertionData, validAssertionDataV2]);
factory.create([validEnvironmentData, validEnvironmentDataV2]);
factory.create([validTestWithEnvironmentsAndAssertions]);
factory.create([validEnvironmentDataV2]);
factory.resolveRefs();
const getTests = factory.getAllTests();
expect(getTests).toHaveLength(2);
});
it('should resolve 2x2 api gateway, 2 environments ', () => {
factory.create(multiGatewayAPIEnv);
factory.create([validAssertionData]);
factory.create([validEnvironmentData]);
factory.create([validTestWithMultiAPIAndEnvironment]);
factory.create([validEnvironmentDataV2]);
factory.resolveRefs();
const getTests = factory.getAllTests();
expect(getTests).toHaveLength(8);
// All test should have unique VCM to store test results
const vcmIds = getTests.map((test) => test.vcmId);
const uniqueIds = new Set(vcmIds);
expect(uniqueIds.size).toBe(vcmIds.length);
});
it('should correctly resolve array of $ref objects in assertions', () => {
factory.create([validAssertionData, validAssertionDataV2]);
factory.create([validEnvironmentData]);
// Create a test with array of assertion ref objects
const testWithArrayOfRefObjects = {
kind: 'test',
metadata: {
name: 'TestWithArrayOfRefObjects',
version: '1.0.0',
namespace: 'default',
},
spec: {
api: {
$endpoint: 'www.test.com',
},
environment: {
$ref: 'default:TestPaymentsEnvironment:1.0.0',
},
request: [
{
method: 'POST',
resource: '/api/resource',
assertions: [
{ $ref: 'default:TestPaymentAssertion:1.0.0' },
{ $ref: 'default:TestPaymentAssertion:2.0.0' },
],
},
],
},
};
factory.create([testWithArrayOfRefObjects]);
factory.resolveRefs();
const tests = factory.getAllTests();
expect(tests).toHaveLength(1);
// Verify that both assertion references were resolved correctly
const assertions1 = factory.getAssertions(
'default',
'TestPaymentAssertion',
'1.0.0',
)?.spec;
const assertions2 = factory.getAssertions(
'default',
'TestPaymentAssertion',
'2.0.0',
)?.spec;
// Check that all assertions from both references are included in the expressions
const assertions = tests[0].spec.request[0].assertions;
const expressions =
assertions && 'assertions' in assertions ? assertions.assertions : [];
assertions1?.forEach((assertion) => {
expect(expressions?.[0].spec).toContainEqual(
expect.objectContaining({
name: assertion.name,
key: assertion.key,
value: assertion.value,
action: assertion.action,
}),
);
});
assertions2?.forEach((assertion) => {
expect(expressions?.[0].spec).toContainEqual(
expect.objectContaining({
name: assertion.name,
key: assertion.key,
value: assertion.value,
action: assertion.action,
}),
);
});
});
it('should resolve API, environment, and assertions for test collection ', () => {
factory.create(gatewayEnv);
factory.create([validAssertionData]);
factory.create([validEnvironmentData]);
// ensure environment is a valid $ref
const patchedValidTests = validTests.map((t) => ({
...t,
spec: {
...t.spec,
environment: { $ref: 'default:TestPaymentsEnvironment:1.0.0' },
},
}));
factory.create(patchedValidTests);
factory.resolveRefs();
const getTests = factory.getAllTests();
expect(getTests).toHaveLength(2);
if ('variables' in patchedValidTests[1].spec.environment) {
expect(
(getTests[0].spec.environment as { variables: any[] }).variables,
).toContainEqual(
(patchedValidTests[1].spec.environment as { variables: any[] })
.variables[0],
);
}
const assertions = factory.getAssertions(
'default',
'TestPaymentAssertion',
'1.0.0',
);
assertions?.spec!.forEach((assert) => {
const requestAssertions0 = getTests[0].spec.request[0].assertions;
const resolvedAssertions1 =
requestAssertions0 && 'assertions' in requestAssertions0
? requestAssertions0.assertions
: [];
expect(resolvedAssertions1).toBeDefined();
if (resolvedAssertions1) {
const allAssertionSpecs1 = resolvedAssertions1.flatMap((a) => a.spec);
expect(allAssertionSpecs1).toContainEqual(assert);
}
const requestAssertions2 = getTests[1].spec.request[0].assertions;
const resolvedAssertions2 =
requestAssertions2 && 'assertions' in requestAssertions2
? requestAssertions2.assertions
: [];
expect(resolvedAssertions2).toBeDefined();
if (resolvedAssertions2) {
const allAssertionSpecs2 = resolvedAssertions2.flatMap((a) => a.spec);
expect(allAssertionSpecs2).toContainEqual(assert);
}
});
});
it('should handle missing environment', () => {
expect(() => factory.create([testWithEmptyEnvironment])).toThrow(
`Validation error at spec.environment.$ref: $ref cannot be an empty string`,
);
});
it('should handle missing assertion', () => {
expect(() => factory.create([testWithEmptyAssertion])).toThrow(
`Validation error at spec.request.[0].assertions.$ref: $ref cannot be an empty string`,
);
});
it('should handle error in resolving values', () => {
factory.create([
{
...validTest,
spec: {
...validTest.spec,
api: {
$ref: 'PaymentAPI:3.0.1',
},
},
},
]);
expect(() => factory.resolveRefs()).toThrow(
'default:TestPaymentAssertion:1.0.0 is not available in assertions',
);
factory.create([validAssertionData]);
expect(() => factory.resolveRefs()).toThrow(
`Reference variable 'PaymentAPI:3.0.1' not defined`,
);
factory.create([
{
'PaymentAPI:3.0.1': [],
},
]);
expect(() => factory.resolveRefs()).not.toThrow();
});
it('should handle invalid environment resolution', () => {
factory.create([
{
...validTest,
spec: {
...validTest.spec,
environment: {
$ref: 'default:TestPaymentsEnvironment:4.0.0',
},
},
},
]);
factory.create(gatewayEnv);
factory.create([validAssertionData]);
expect(() => factory.resolveRefs()).toThrow(
'default:TestPaymentsEnvironment:4.0.0 is not available in environment',
);
});
it('should do nothing if no test s are present', () => {
jest.spyOn(factory, 'getAllTests').mockReturnValue([]);
jest.spyOn(factory as any, 'resolveAPI').mockImplementation(jest.fn());
jest
.spyOn(factory as any, 'resolveEnvironment')
.mockImplementation(jest.fn());
jest
.spyOn(factory as any, 'resolveAssertions')
.mockImplementation(jest.fn());
factory.resolveRefs();
expect((factory as any).resolveAPI).not.toHaveBeenCalled();
expect((factory as any).resolveEnvironment).not.toHaveBeenCalled();
expect((factory as any).resolveAssertions).not.toHaveBeenCalled();
});
it('should handle a single test gracefully', () => {
jest
.spyOn(factory, 'getAllTestsWithKey')
.mockReturnValue([{ key: 'key1', test: validTests[0] as Test }]);
jest.spyOn(factory as any, 'resolveAPI').mockImplementation(jest.fn());
jest
.spyOn(factory as any, 'resolveEnvironment')
.mockImplementation(jest.fn());
jest
.spyOn(factory as any, 'resolveAssertions')
.mockImplementation(jest.fn());
factory.resolveRefs();
expect((factory as any).resolveAPI).toHaveBeenCalledTimes(1);
expect((factory as any).resolveEnvironment).toHaveBeenCalledTimes(1);
expect((factory as any).resolveAssertions).toHaveBeenCalledTimes(1);
});
it('should handle clearing the factory, when destroy is called', () => {
factory.create([validTest]);
factory.destroy();
expect(factory.getAllTests()).toHaveLength(0);
});
});