Skip to content

Commit

Permalink
feat: unit test and linting for cts generation APIC-277 (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
millotp authored Jan 25, 2022
1 parent a6cfddd commit 76a11b3
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 81 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ jobs:
with:
job: cts

- name: Check script linting
run: yarn cts:lint:scripts

- name: Test CTS script
run: yarn cts:test:scripts

- name: Generate CTS
run: yarn cts:generate

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"clean": "rm -rf **/dist **/build **/node_modules **/target",
"cts:generate": "yarn workspace tests build && ./scripts/multiplexer.sh ${2:-nonverbose} yarn workspace tests generate ${0:-all} ${1:-all}",
"cts:test": "./scripts/multiplexer.sh ${1:-nonverbose} ./scripts/runCTS.sh ${0:-javascript} all",
"cts:test:scripts": "yarn workspace tests test:scripts",
"cts:lint:scripts": "eslint --ext=ts tests/src/",
"docker:build": "./scripts/docker/build.sh",
"docker:clean": "docker stop dev; docker rm -f dev; docker image rm -f api-clients-automation",
"docker:mount": "./scripts/docker/mount.sh",
Expand Down
8 changes: 8 additions & 0 deletions scripts/multiplexer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,11 @@ for lang in "${LANGUAGE[@]}"; do
fi
done
done

# Format after every client for the CTS
if [[ $CMD == 'yarn workspace tests generate' ]]; then
for lang in "${LANGUAGE[@]}"; do
yarn workspace tests format $lang
done
yarn cts:lint:scripts --fix
fi
6 changes: 6 additions & 0 deletions tests/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['output'],
};
4 changes: 0 additions & 4 deletions tests/output/javascript/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
"typeRoots": [
"../../../node_modules/@types"
],
"types": [
"node",
"jest"
],
"resolveJsonModule": true
},
"include": [
Expand Down
8 changes: 6 additions & 2 deletions tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@
"build": "tsc",
"generate:methods:requets": "node dist/tests/src/methods/requests/main.js ${0:-javascript} ${1:-search}",
"format": "../scripts/formatter.sh ${0:-javascript} tests/output/${0:-javascript}",
"generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search} && yarn format ${0:-javascript}",
"start": "yarn build && yarn generate ${0:-javascript} ${1:-search}"
"generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search}",
"start": "yarn build && yarn generate ${0:-javascript} ${1:-search}",
"test:scripts": "jest"
},
"devDependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@types/jest": "27.4.0",
"@types/mustache": "4.1.2",
"@types/node": "16.11.11",
"eslint": "8.6.0",
"jest": "27.4.7",
"mustache": "4.2.0",
"openapi-types": "10.0.0",
"ts-jest": "27.1.3",
"typescript": "4.5.4"
}
}
210 changes: 167 additions & 43 deletions tests/src/methods/requests/cts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,143 @@ import fsp from 'fs/promises';
import SwaggerParser from '@apidevtools/swagger-parser';
import type { OpenAPIV3 } from 'openapi-types';

import { removeObjectName, walk } from '../../utils';
import { removeEnumType, removeObjectName, walk } from '../../utils';

import type {
CTS,
CTSBlock,
ParametersWithDataType,
RequestCTS,
RequestCTSOutput,
} from './types';

/**
* Provide the `key` and `is*` params to apply custom logic in templates
* include the `-last` param to join with comma in mustache.
*/
function transformParam({
key = '$root',
value,
last = true,
testName,
parent,
suffix = 0,
}: {
key?: string;
value: any;
last?: boolean;
testName: string;
parent?: string;
suffix?: number;
}): ParametersWithDataType | ParametersWithDataType[] {
const isDate = key === 'endAt';
const isArray = Array.isArray(value);
let isObject = typeof value === 'object' && !isArray;
const isEnum = isObject && '$enumType' in value;
const isString = typeof value === 'string' && !isDate;
const isBoolean = typeof value === 'boolean';
const isInteger = Number.isInteger(value);
const isDouble = typeof value === 'number' && !isInteger;
const objectName: string | undefined = (value as any).$objectName;
const isFreeFormObject = objectName === 'Object';

if (isEnum) {
isObject = false;
}

const isTypes = {
isArray,
isObject: isObject && !isFreeFormObject,
isFreeFormObject,
isEnum,
isString,
isBoolean,
isInteger,
isDouble,
};

const isRoot = key === '$root';

let out = value;
if (isEnum) {
out = { enumType: value.$enumType, value: value.value };
} else if (isObject) {
// recursive on every key:value
out = Object.entries(value)
.filter(([prop]) => prop !== '$objectName')
.map(([inKey, inValue], i, arr) =>
transformParam({
key: inKey,
value: inValue,
last: i === arr.length - 1,
testName,
parent: isRoot ? 'param' : key,
suffix: suffix + 1,
})
);

// Special case for root
if (isRoot) {
if (objectName) {
return {
key: 'param',
value: out,
objectName,
suffix,
parentSuffix: suffix,
...isTypes,
'-last': true,
};
}
return out;
}

if (!objectName) {
// throw new Error(`Object ${key} missing property $objectName in test ${testName}`);
// eslint-disable-next-line no-console
console.log(
`Object ${key} missing property $objectName in test ${testName}`
);
}
} else if (isArray) {
// recursive on all value
out = value.map((v, i) =>
transformParam({
key: `${key}Param${i}`,
value: v,
last: i === value.length - 1,
testName,
parent: key,
suffix: suffix + 1,
})
);
}

return {
key,
value: out,
objectName,
parent,
suffix,
parentSuffix: suffix - 1,
...isTypes,
'-last': last,
};
}

import type { CTS, CTSBlock, Tests } from './types';
function createParamWithDataType({
parameters,
testName,
}: {
parameters: Record<string, any>;
testName: string;
}): ParametersWithDataType[] {
const transformed = transformParam({ value: parameters, testName });
if (Array.isArray(transformed)) {
return transformed;
}
return [transformed];
}

async function loadRequestsCTS(client: string): Promise<CTSBlock[]> {
// load the list of operations from the spec
Expand All @@ -31,67 +165,57 @@ async function loadRequestsCTS(client: string): Promise<CTSBlock[]> {
throw new Error(`cannot read empty file ${fileName} - ${client} client`);
}

const tests: Tests[] = JSON.parse(fileContent);
const tests: RequestCTS[] = JSON.parse(fileContent);

// check test validity against spec
if (!operations.includes(fileName)) {
throw new Error(`cannot find ${fileName} for the ${client} client`);
}

const testsOutput: RequestCTSOutput[] = [];
let testIndex = 0;
for (const test of tests) {
if (test.testName === undefined) {
test.testName = test.method;
}
const testOutput = test as RequestCTSOutput;
testOutput.testName = test.testName || test.method;
testOutput.testIndex = testIndex++;

// stringify request.data too
test.request.data = JSON.stringify(test.request.data);
test.request.searchParams = JSON.stringify(test.request.searchParams);

if (Object.keys(test.parameters).length === 0) {
test.parameters = undefined;
test.parametersWithDataType = undefined;
test.hasParameters = false;

continue;
}
testOutput.request.data = JSON.stringify(test.request.data);
testOutput.request.searchParams = JSON.stringify(
test.request.searchParams
);

if (
typeof test.parameters !== 'object' ||
Array.isArray(test.parameters)
) {
throw new Error(`parameters of ${test.testName} must be an object`);
throw new Error(
`parameters of ${testOutput.testName} must be an object`
);
}

// we stringify the param for mustache to render them properly
// delete the object name recursively for now, but it could be use for `new $objectName(params)`
removeObjectName(test.parameters);

// Provide the `key` and `is*` params to apply custom logic in templates
// include the `-last` param to join with comma in mustache
test.parametersWithDataType = Object.entries(test.parameters).map(
([key, value], i, arr) => {
const isDate = key === 'endAt';
const isArray = Array.isArray(value);

return {
key,
value: JSON.stringify(value),
isString: typeof value === 'string' && isDate === false,
isObject: typeof value === 'object' && isArray === false,
isArray,
isDate,
'-last': i === arr.length - 1,
};
}
);

test.parameters = JSON.stringify(test.parameters);
test.hasParameters = true;
if (Object.keys(test.parameters).length === 0) {
testOutput.parameters = undefined;
testOutput.parametersWithDataType = undefined;
testOutput.hasParameters = false;
} else {
testOutput.parametersWithDataType = createParamWithDataType({
parameters: test.parameters,
testName: testOutput.testName,
});

// we stringify the param for mustache to render them properly
testOutput.parameters = JSON.stringify(
removeEnumType(removeObjectName(test.parameters))
);
testOutput.hasParameters = true;
}
testsOutput.push(testOutput);
}

ctsClient.push({
operationId: fileName,
tests,
tests: testsOutput,
});
}

Expand Down
Loading

0 comments on commit 76a11b3

Please sign in to comment.