@apistudio/apim-cli
Version:
CLI for API Management Products
467 lines (402 loc) • 13.7 kB
text/typescript
/**
* Copyright IBM Corp. 2024, 2025
*/
import {
ContextManager,
VCM,
} from '../../../src/engine/variable-context-manager/context-manager.js';
describe('ContextManager with multiple contexts', () => {
let manager: ContextManager;
beforeAll(() => {
manager = new ContextManager();
});
afterEach(() => {
manager.clearAll();
});
it('should isolate data between contexts', () => {
const contextA = manager.createContext('contextA');
const contextB = manager.createContext('contextB');
manager.getGlobalContext().setVariable('env', 'prod');
contextA.setVariable('user', 'Alice');
contextB.setVariable('user', 'Bob');
expect(manager.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
expect(manager.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
expect(manager.resolve('contextA', 'Env: ${env}')).toBe('Env: prod');
expect(manager.resolve('contextA', 'Env: ${env}')).toBe('Env: prod');
expect(() => manager.resolve('contextC', 'Env: ${env}')).toThrow();
const contexts = manager.listContexts();
expect(contexts.sort()).toEqual(['contextA', 'contextB']);
});
it('should support array index path resolution for given expression', () => {
const input = [{ name: 'Alice' }, { name: 'Bob' }];
const context = manager.createContext('s-array');
context.setVariable('users', input);
const resolved = manager.resolve(
's-array',
'First: ${users.0.name}, Second: ${users.1.name}',
);
expect(() => manager.resolveExpression('s-array', null)).toThrow();
expect(manager.resolveExpression('s-array', 'users')).toBe(input);
expect(manager.resolveExpression('s-array', 'users.0.name')).toBe('Alice');
expect(manager.resolve('s-array', '${users.0.name}')).toBe('Alice');
expect(resolved).toBe('First: Alice, Second: Bob');
expect(manager.resolve('s-array', '${users.0.name}A')).toBe('AliceA');
});
it('should resolve given string', () => {
const input = 'Alice';
const context = manager.createContext('s-array');
context.setVariable('users', input);
expect(manager.resolve('s-array', '${users}A')).toBe('AliceA');
});
it('check input as undefined', () => {
manager.createContext('s-array');
expect(manager.resolve('s-array', undefined)).toBe(undefined);
});
});
describe('ContextManager - Negative & Error Scenarios', () => {
let manager: ContextManager;
beforeAll(() => {
manager = new ContextManager();
});
afterEach(() => {
manager.clearAll();
});
it('should throw error for no context', () => {
expect(() => {
manager.resolve('test', 'Hello ${missing}');
}).toThrow("Context 'test' not found.");
expect(() => {
manager.resolveExpression('test', 'Hello ${missing}');
}).toThrow("Context 'test' not found.");
});
it('should throw context exists', () => {
expect(() => {
manager.createContext('test');
manager.createContext('test');
}).toBeDefined();
});
it('should not throw error for undefined variable', () => {
expect(() => {
manager.getContext('test');
manager.resolve('test', 'Hello ${missing}');
}).not.toThrow();
});
it('should not throw error for empty variable', () => {
manager.createContext('test');
expect(() => {
manager.resolve('test', '');
}).not.toThrow();
expect(manager.resolve('test', '')).toEqual('');
expect(() => manager.resolve('test', '${}')).not.toThrow();
expect(manager.resolve('test', '${}')).toEqual('');
expect(() => manager.resolve('test', '${""}')).not.toThrow();
expect(manager.resolve('test', '${""}')).toBeUndefined();
});
it('should not throw error for empty variable in expression', () => {
manager.createContext('test');
expect(() => {
manager.resolveExpression('test', '');
}).not.toThrow();
expect(manager.resolveExpression('test', '')).toEqual('');
expect(() => manager.resolveExpression('test', '${}')).not.toThrow();
expect(manager.resolveExpression('test', '${}')).toBeUndefined();
expect(() => manager.resolveExpression('test', '${""}')).not.toThrow();
expect(manager.resolveExpression('test', '${""}')).toBeUndefined();
});
it('should handle invalid path on object gracefully', () => {
const context = manager.createContext('test');
context.setVariable('user', { name: 'Alice' });
const result = manager.resolve('test', 'Hello ${user.age.year}');
expect(result).toBe('Hello ');
});
it('should handle invalid array index gracefully', () => {
const context = manager.createContext('array');
context.setVariable('items', ['apple', 'banana']);
const result = manager.resolve('array', 'Item: ${items.5}');
expect(result).toBe('Item: ');
});
});
describe('VCM - Validate all scenarios using global store', () => {
beforeAll(() => {
VCM.createContext('contextA').setVariable('user', 'Alice');
VCM.createContext('contextB').setVariable('user', 'Bob');
VCM.createContext('contextA')?.setVariable('userJson', {
name: 'Alice',
address: { city: 'London', country: 'UK' },
});
VCM.createContext('contextA')?.setVariable('userLocJson', {
cityKey: 'city',
});
VCM.createContext('contextA')?.setVariable('fruits', [
'apple',
'banana',
'cherry',
]);
VCM.createContext('contextA')?.setVariable('userFav', {
favorites: { index: 1 },
});
VCM.createContext('contextA')?.setVariable('scores', [10, false, 30]);
});
it('should isolate data between contexts and fetch using VCM', () => {
expect(VCM.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
expect(VCM.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
});
it('should resolve JSON using VCM', () => {
const input = {
greeting: 'Hello ${userJson.name}',
details: {
city: '${userJson.address.city}',
},
};
const output = {
greeting: 'Hello Alice',
details: {
city: 'London',
},
};
expect(VCM.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
expect(VCM.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
expect(VCM.resolve('contextA', input)).toEqual(output);
});
it('should resolve JSON with nested using VCM', () => {
const input = {
greeting: 'Hello ${userJson.name}',
details: {
city: '${userJson.address.${userLocJson.cityKey}}',
},
};
const output = {
greeting: 'Hello Alice',
details: {
city: 'London',
},
};
expect(VCM.resolve('contextA', 'User: ${user}')).toBe('User: Alice');
expect(VCM.resolve('contextB', 'User: ${user}')).toBe('User: Bob');
expect(VCM.resolve('contextA', input)).toEqual(output);
});
it('should resolve Array using VCM with text and variable replacement', () => {
const input = ['first ${fruits.1}'];
const output = ['first banana'];
expect(VCM.resolve('contextA', input)).toEqual(output);
});
it('should resolve Array using VCM with full multiple variable replacement', () => {
const input = ['${fruits.0} ${fruits.1}'];
const output = ['apple banana'];
expect(VCM.resolve('contextA', input)).toEqual(output);
});
it('should resolve Array using VCM with nested variable replacement', () => {
const input = ['${fruits.${userFav.favorites.index}}'];
const output = ['banana'];
expect(VCM.resolve('contextA', input)).toEqual(output);
});
it('should resolve Array using VCM with text and nest variable replacement', () => {
const input = ['Second: ${fruits.${userFav.favorites.index}}'];
const output = ['Second: banana'];
expect(VCM.resolve('contextA', input)).toEqual(output);
});
it('should resolve arrays inside an object structure', () => {
const input = {
best: '${scores.2}',
worst: '${scores.0}',
bool: '${scores.1}',
};
const expected = {
best: 30,
worst: 10,
bool: false,
};
expect(VCM.resolve('contextA', input)).toEqual(expected);
});
it('should handle out-of-bound index gracefully', () => {
expect(VCM.resolve('contextA', 'Invalid: num ${nums.5}')).toBe(
'Invalid: num ',
);
});
it('Clean all and check no state is available', () => {
VCM.clearAll();
expect(() => {
VCM.resolve('test', 'Hello ${missing}');
}).toThrow("Context 'test' not found.");
});
it('should throw error for no context', () => {
expect(() => {
VCM.resolve('test', 'Hello ${missing}');
}).toThrow("Context 'test' not found.");
});
it('should handle undefined variable gracefully', () => {
VCM.createContext('test');
expect(VCM.resolve('test', 'Hello ${missing}')).toBe('Hello ');
});
});
describe('VCM.loadEnv', () => {
beforeAll(() => {
VCM.createContext('testContext');
});
it('should load environment variables into testContext', () => {
const envVars = [
{
key: 'content-type',
value: 'application/json',
isSecret: true,
},
{
key: 'user',
value: 'Alice',
isSecret: false,
},
];
VCM.loadEnv('testContext', envVars);
const context = VCM.createContext('testContext');
expect(context).toBeDefined();
expect(context?.get('user')?.value).toBe('Alice');
expect(context?.getValue('content-type')).toBe('application/json');
expect(context?.get('content-type')).toEqual({
value: 'application/json',
isSecret: true,
});
});
it('should load variables into testContext', () => {
const envVars = [
{
key: 'content-type',
value: 'application/json',
isSecret: true,
},
{
key: 'user',
value: 'Alice',
isSecret: false,
},
{
key: 'method',
value: 'GET',
},
];
VCM.loadEnv('testContext', envVars);
const context = VCM.createContext('testContext');
expect(context).toBeDefined();
expect(context?.get('user')?.value).toBe('Alice');
expect(context?.getValue('content-type')).toBe('application/json');
expect(context?.get('content-type')).toEqual({
value: 'application/json',
isSecret: true,
});
});
it('should resolve "TEST" request model', () => {
const input = {
request: [
{
method: 'POST',
resource: 'v2/pet',
headers: [
{
key: 'Content-Type',
value: '${content-type}',
},
],
payload: {
raw: {
json: '{\n "name":"Jose"\n}\n',
},
},
},
{
method: '${method}',
headers: [
{
key: 'Content-Type',
value: '${content-type}',
},
],
payload: {
raw: {
json: { name: 'Adam' },
},
},
},
],
};
const output = {
request: [
{
method: 'POST',
resource: 'v2/pet',
headers: [
{
key: 'Content-Type',
value: 'application/json',
},
],
payload: {
raw: {
json: '{\n "name":"Jose"\n}\n',
},
},
},
{
method: 'GET',
headers: [
{
key: 'Content-Type',
value: 'application/json',
},
],
payload: {
raw: {
json: { name: 'Adam' },
},
},
},
],
};
expect(VCM.resolve('testContext', input)).toEqual(output);
});
it('should load json variables into testContext', () => {
const envVars = {
user: 'Alice',
age: 30,
isAdmin: true,
nested: { city: 'London' },
};
VCM.loadEnv('testContext', envVars);
const context = VCM.createContext('testContext');
expect(context).toBeDefined();
expect(context?.getValue('user')).toBe('Alice');
expect(context?.getValue('age')).toBe(30);
expect(context?.getValue('isAdmin')).toBe(true);
expect(context?.getValue('nested')).toEqual({ city: 'London' });
});
it('sets key-value pairs with { value, isSecret } format', () => {
const input = {
A: { value: 'secretVal', isSecret: true },
B: { value: 42, isSecret: false },
};
VCM.loadEnv('testContext', input);
const ctx = VCM.createContext('testContext');
expect(ctx?.getValue('A')).toBe('secretVal');
expect(ctx?.getValue('B')).toBe(42);
});
it('sets key-value pairs with { value, isSecret } format', () => {
const input = {
A: { value: 'secretVal', isSecret: true },
};
VCM.loadEnv('testContext', input);
const ctx = VCM.createContext('testContext');
expect(ctx?.getValue('A')).toBe('secretVal');
expect(ctx?.getValue('B')).toBe(42);
});
it('should overwrite existing context if loadEnv called again with same contextId', () => {
VCM.loadEnv('testContext', { user: 'Bob' });
let context = VCM.createContext('testContext');
expect(context?.getValue('user')).toBe('Bob');
// Load again with different data
VCM.loadEnv('testContext', { user: 'Carol', isAdmin: false });
context = VCM.createContext('testContext');
expect(context?.getValue('user')).toBe('Carol');
expect(context?.getValue('isAdmin')).toBe(false);
expect(context?.getValue('age')).toBe(30);
context?.delete('age');
expect(context?.getValue('age')).toBeUndefined();
});
});