Skip to content

Commit 76a11b3

Browse files
authored
feat: unit test and linting for cts generation APIC-277 (#103)
1 parent a6cfddd commit 76a11b3

File tree

14 files changed

+478
-81
lines changed

14 files changed

+478
-81
lines changed

.github/workflows/check.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ jobs:
149149
with:
150150
job: cts
151151

152+
- name: Check script linting
153+
run: yarn cts:lint:scripts
154+
155+
- name: Test CTS script
156+
run: yarn cts:test:scripts
157+
152158
- name: Generate CTS
153159
run: yarn cts:generate
154160

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"clean": "rm -rf **/dist **/build **/node_modules **/target",
1414
"cts:generate": "yarn workspace tests build && ./scripts/multiplexer.sh ${2:-nonverbose} yarn workspace tests generate ${0:-all} ${1:-all}",
1515
"cts:test": "./scripts/multiplexer.sh ${1:-nonverbose} ./scripts/runCTS.sh ${0:-javascript} all",
16+
"cts:test:scripts": "yarn workspace tests test:scripts",
17+
"cts:lint:scripts": "eslint --ext=ts tests/src/",
1618
"docker:build": "./scripts/docker/build.sh",
1719
"docker:clean": "docker stop dev; docker rm -f dev; docker image rm -f api-clients-automation",
1820
"docker:mount": "./scripts/docker/mount.sh",

scripts/multiplexer.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,11 @@ for lang in "${LANGUAGE[@]}"; do
9292
fi
9393
done
9494
done
95+
96+
# Format after every client for the CTS
97+
if [[ $CMD == 'yarn workspace tests generate' ]]; then
98+
for lang in "${LANGUAGE[@]}"; do
99+
yarn workspace tests format $lang
100+
done
101+
yarn cts:lint:scripts --fix
102+
fi

tests/jest.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
testPathIgnorePatterns: ['output'],
6+
};

tests/output/javascript/tsconfig.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
"typeRoots": [
66
"../../../node_modules/@types"
77
],
8-
"types": [
9-
"node",
10-
"jest"
11-
],
128
"resolveJsonModule": true
139
},
1410
"include": [

tests/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@
88
"build": "tsc",
99
"generate:methods:requets": "node dist/tests/src/methods/requests/main.js ${0:-javascript} ${1:-search}",
1010
"format": "../scripts/formatter.sh ${0:-javascript} tests/output/${0:-javascript}",
11-
"generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search} && yarn format ${0:-javascript}",
12-
"start": "yarn build && yarn generate ${0:-javascript} ${1:-search}"
11+
"generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search}",
12+
"start": "yarn build && yarn generate ${0:-javascript} ${1:-search}",
13+
"test:scripts": "jest"
1314
},
1415
"devDependencies": {
1516
"@apidevtools/swagger-parser": "10.0.3",
17+
"@types/jest": "27.4.0",
1618
"@types/mustache": "4.1.2",
1719
"@types/node": "16.11.11",
1820
"eslint": "8.6.0",
21+
"jest": "27.4.7",
1922
"mustache": "4.2.0",
2023
"openapi-types": "10.0.0",
24+
"ts-jest": "27.1.3",
2125
"typescript": "4.5.4"
2226
}
2327
}

tests/src/methods/requests/cts.ts

Lines changed: 167 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,143 @@ import fsp from 'fs/promises';
33
import SwaggerParser from '@apidevtools/swagger-parser';
44
import type { OpenAPIV3 } from 'openapi-types';
55

6-
import { removeObjectName, walk } from '../../utils';
6+
import { removeEnumType, removeObjectName, walk } from '../../utils';
7+
8+
import type {
9+
CTS,
10+
CTSBlock,
11+
ParametersWithDataType,
12+
RequestCTS,
13+
RequestCTSOutput,
14+
} from './types';
15+
16+
/**
17+
* Provide the `key` and `is*` params to apply custom logic in templates
18+
* include the `-last` param to join with comma in mustache.
19+
*/
20+
function transformParam({
21+
key = '$root',
22+
value,
23+
last = true,
24+
testName,
25+
parent,
26+
suffix = 0,
27+
}: {
28+
key?: string;
29+
value: any;
30+
last?: boolean;
31+
testName: string;
32+
parent?: string;
33+
suffix?: number;
34+
}): ParametersWithDataType | ParametersWithDataType[] {
35+
const isDate = key === 'endAt';
36+
const isArray = Array.isArray(value);
37+
let isObject = typeof value === 'object' && !isArray;
38+
const isEnum = isObject && '$enumType' in value;
39+
const isString = typeof value === 'string' && !isDate;
40+
const isBoolean = typeof value === 'boolean';
41+
const isInteger = Number.isInteger(value);
42+
const isDouble = typeof value === 'number' && !isInteger;
43+
const objectName: string | undefined = (value as any).$objectName;
44+
const isFreeFormObject = objectName === 'Object';
45+
46+
if (isEnum) {
47+
isObject = false;
48+
}
49+
50+
const isTypes = {
51+
isArray,
52+
isObject: isObject && !isFreeFormObject,
53+
isFreeFormObject,
54+
isEnum,
55+
isString,
56+
isBoolean,
57+
isInteger,
58+
isDouble,
59+
};
60+
61+
const isRoot = key === '$root';
62+
63+
let out = value;
64+
if (isEnum) {
65+
out = { enumType: value.$enumType, value: value.value };
66+
} else if (isObject) {
67+
// recursive on every key:value
68+
out = Object.entries(value)
69+
.filter(([prop]) => prop !== '$objectName')
70+
.map(([inKey, inValue], i, arr) =>
71+
transformParam({
72+
key: inKey,
73+
value: inValue,
74+
last: i === arr.length - 1,
75+
testName,
76+
parent: isRoot ? 'param' : key,
77+
suffix: suffix + 1,
78+
})
79+
);
80+
81+
// Special case for root
82+
if (isRoot) {
83+
if (objectName) {
84+
return {
85+
key: 'param',
86+
value: out,
87+
objectName,
88+
suffix,
89+
parentSuffix: suffix,
90+
...isTypes,
91+
'-last': true,
92+
};
93+
}
94+
return out;
95+
}
96+
97+
if (!objectName) {
98+
// throw new Error(`Object ${key} missing property $objectName in test ${testName}`);
99+
// eslint-disable-next-line no-console
100+
console.log(
101+
`Object ${key} missing property $objectName in test ${testName}`
102+
);
103+
}
104+
} else if (isArray) {
105+
// recursive on all value
106+
out = value.map((v, i) =>
107+
transformParam({
108+
key: `${key}Param${i}`,
109+
value: v,
110+
last: i === value.length - 1,
111+
testName,
112+
parent: key,
113+
suffix: suffix + 1,
114+
})
115+
);
116+
}
117+
118+
return {
119+
key,
120+
value: out,
121+
objectName,
122+
parent,
123+
suffix,
124+
parentSuffix: suffix - 1,
125+
...isTypes,
126+
'-last': last,
127+
};
128+
}
7129

8-
import type { CTS, CTSBlock, Tests } from './types';
130+
function createParamWithDataType({
131+
parameters,
132+
testName,
133+
}: {
134+
parameters: Record<string, any>;
135+
testName: string;
136+
}): ParametersWithDataType[] {
137+
const transformed = transformParam({ value: parameters, testName });
138+
if (Array.isArray(transformed)) {
139+
return transformed;
140+
}
141+
return [transformed];
142+
}
9143

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

34-
const tests: Tests[] = JSON.parse(fileContent);
168+
const tests: RequestCTS[] = JSON.parse(fileContent);
35169

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

175+
const testsOutput: RequestCTSOutput[] = [];
176+
let testIndex = 0;
41177
for (const test of tests) {
42-
if (test.testName === undefined) {
43-
test.testName = test.method;
44-
}
178+
const testOutput = test as RequestCTSOutput;
179+
testOutput.testName = test.testName || test.method;
180+
testOutput.testIndex = testIndex++;
45181

46182
// stringify request.data too
47-
test.request.data = JSON.stringify(test.request.data);
48-
test.request.searchParams = JSON.stringify(test.request.searchParams);
49-
50-
if (Object.keys(test.parameters).length === 0) {
51-
test.parameters = undefined;
52-
test.parametersWithDataType = undefined;
53-
test.hasParameters = false;
54-
55-
continue;
56-
}
183+
testOutput.request.data = JSON.stringify(test.request.data);
184+
testOutput.request.searchParams = JSON.stringify(
185+
test.request.searchParams
186+
);
57187

58188
if (
59189
typeof test.parameters !== 'object' ||
60190
Array.isArray(test.parameters)
61191
) {
62-
throw new Error(`parameters of ${test.testName} must be an object`);
192+
throw new Error(
193+
`parameters of ${testOutput.testName} must be an object`
194+
);
63195
}
64196

65-
// we stringify the param for mustache to render them properly
66-
// delete the object name recursively for now, but it could be use for `new $objectName(params)`
67-
removeObjectName(test.parameters);
68-
69-
// Provide the `key` and `is*` params to apply custom logic in templates
70-
// include the `-last` param to join with comma in mustache
71-
test.parametersWithDataType = Object.entries(test.parameters).map(
72-
([key, value], i, arr) => {
73-
const isDate = key === 'endAt';
74-
const isArray = Array.isArray(value);
75-
76-
return {
77-
key,
78-
value: JSON.stringify(value),
79-
isString: typeof value === 'string' && isDate === false,
80-
isObject: typeof value === 'object' && isArray === false,
81-
isArray,
82-
isDate,
83-
'-last': i === arr.length - 1,
84-
};
85-
}
86-
);
87-
88-
test.parameters = JSON.stringify(test.parameters);
89-
test.hasParameters = true;
197+
if (Object.keys(test.parameters).length === 0) {
198+
testOutput.parameters = undefined;
199+
testOutput.parametersWithDataType = undefined;
200+
testOutput.hasParameters = false;
201+
} else {
202+
testOutput.parametersWithDataType = createParamWithDataType({
203+
parameters: test.parameters,
204+
testName: testOutput.testName,
205+
});
206+
207+
// we stringify the param for mustache to render them properly
208+
testOutput.parameters = JSON.stringify(
209+
removeEnumType(removeObjectName(test.parameters))
210+
);
211+
testOutput.hasParameters = true;
212+
}
213+
testsOutput.push(testOutput);
90214
}
91215

92216
ctsClient.push({
93217
operationId: fileName,
94-
tests,
218+
tests: testsOutput,
95219
});
96220
}
97221

0 commit comments

Comments
 (0)