-
Notifications
You must be signed in to change notification settings - Fork 24
feat: common test suite generation APIC-186 #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
06aef6f
first test of cts
millotp db0b72a
upgrade jest
millotp 99266f1
pin dependency
millotp a8cdfb2
review
millotp 9fe0bad
lock yarn
millotp 30f544a
stricter type and prettier
millotp 465e5ea
new line
millotp c50c3fd
prettier
millotp 41ab13a
review
millotp 84b369f
change to testName
millotp f19ecc1
merge with main
millotp 80796fc
fix linting
millotp 942fe9a
fix package json
millotp 9c2bf5d
merge with main
millotp 687f60d
review
millotp 0b4c7b1
Merge branch 'main' into feat/cts
millotp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,5 @@ yarn-error.log | |
**/dist | ||
**/.openapi-generator-ignore | ||
**/git_push.sh | ||
|
||
.vscode |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Common Test Suite | ||
|
||
The CTS aims at ensuring minimal working operation for the API clients, by comparing the request formed by sample parameters. | ||
It is automaticaly generated for all languages, from a JSON entry point. | ||
|
||
## How to run it | ||
|
||
```bash | ||
yarn cts:generate | ||
yarn cts:test | ||
``` | ||
|
||
If you only want to generate the tests for a set of languages, you can run: | ||
|
||
```bash | ||
yarn cts:generate "javascript ruby" | ||
``` | ||
|
||
## How to add test | ||
|
||
The test generation script requires a JSON file name from the `operationId` (e.g. `search.json`), located in the `CTS/<client>/` folder (e.g. `CTS/search/`). | ||
|
||
```json | ||
[ | ||
{ | ||
"testName": "the name of the test (e.g. test('search endpoint'))", | ||
"method": "the method to call (e.g. search)", | ||
"parameters": [ | ||
"indexName", | ||
{ | ||
"$objectName": "the name of the object for strongly type language", | ||
"query": "the string to search" | ||
} | ||
], | ||
"request": { | ||
"path": "/1/indexes/indexName/query", | ||
"method": "POST", | ||
"data": { "query": "the string to search" } | ||
} | ||
} | ||
] | ||
``` | ||
|
||
And that's it! If the name of the file matches a real `operationId` in the spec, then a test will be generated. | ||
|
||
## How to add a new language | ||
|
||
- Create a template in `test/CTS/templates/<your language>.mustache` that parse a array of test into your test framework of choice | ||
- Add the language in the array `languages` in `tests/generateCTS.ts`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[ | ||
{ | ||
"testName": "search", | ||
"method": "search", | ||
"parameters": [ | ||
"indexName", | ||
{ | ||
"$objectName": "Query", | ||
"query": "queryString" | ||
} | ||
], | ||
"request": { | ||
"path": "/1/indexes/indexName/query", | ||
"method": "POST", | ||
"data": { | ||
"query": "queryString" | ||
} | ||
} | ||
} | ||
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { {{client}}, EchoRequester } from '{{{import}}}'; | ||
|
||
describe('Common Test Suite', () => { | ||
const client = new {{client}}(process.env.ALGOLIA_APPLICATION_ID, process.env.ALGOLIA_SEARCH_KEY, { requester: new EchoRequester() }); | ||
|
||
{{#tests}} | ||
test('{{testName}}', async () => { | ||
const req = await client.{{method}}({{#parameters}}{{{value}}}{{^-last}}, {{/-last}}{{/parameters}}); | ||
expect(req).toMatchObject({ | ||
path: '{{{request.path}}}', | ||
method: '{{{request.method}}}', | ||
data: {{{request.data}}}, | ||
}) | ||
}); | ||
|
||
{{/tests}} | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
/* eslint-disable no-console */ | ||
import fsp from 'fs/promises'; | ||
import path from 'path'; | ||
|
||
import Mustache from 'mustache'; | ||
import type { OpenAPIV3 } from 'openapi-types'; | ||
import SwaggerParser from 'swagger-parser'; | ||
|
||
import openapitools from '../openapitools.json'; | ||
|
||
const availableLanguages = ['javascript'] as const; | ||
type Language = typeof availableLanguages[number]; | ||
|
||
type CTSBlock = { | ||
name: string; | ||
method: string; | ||
parameters: any[]; | ||
request: { | ||
path: string; | ||
method: string; | ||
data: string; | ||
}; | ||
}; | ||
|
||
// Array of test per client | ||
type CTS = Record<string, CTSBlock[]>; | ||
|
||
const extensionForLanguage: Record<Language, string> = { | ||
javascript: '.test.ts', | ||
}; | ||
|
||
const cts: CTS = {}; | ||
|
||
// For each generator, we map the packageName with the language and client | ||
const packageNames: Record<string, Record<Language, string>> = Object.entries( | ||
openapitools['generator-cli'].generators | ||
).reduce((prev, [clientName, clientConfig]) => { | ||
const obj = prev; | ||
const [lang, client] = clientName.split('-') as [Language, string]; | ||
|
||
if (!(lang in prev)) { | ||
obj[lang] = {}; | ||
} | ||
|
||
obj[lang][client] = clientConfig.additionalProperties.packageName; | ||
|
||
return obj; | ||
}, {} as Record<string, Record<string, string>>); | ||
|
||
async function createOutputDir(language: Language): Promise<void> { | ||
await fsp.mkdir(`output/${language}`, { recursive: true }); | ||
millotp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
async function* walk( | ||
dir: string | ||
): AsyncGenerator<{ path: string; name: string }> { | ||
for await (const d of await fsp.opendir(dir)) { | ||
const entry = path.join(dir, d.name); | ||
if (d.isDirectory()) yield* walk(entry); | ||
else if (d.isFile()) yield { path: entry, name: d.name }; | ||
} | ||
} | ||
|
||
function capitalize(str: string): string { | ||
return str.charAt(0).toUpperCase() + str.slice(1); | ||
} | ||
|
||
async function loadCTSForClient(client: string): Promise<CTSBlock[]> { | ||
// load the list of operations from the spec | ||
const spec = await SwaggerParser.validate(`../specs/${client}/spec.yml`); | ||
const operations = Object.values(spec.paths) | ||
.flatMap<OpenAPIV3.OperationObject>((p) => Object.values(p)) | ||
.map((obj) => obj.operationId); | ||
|
||
const ctsClient: CTSBlock[] = []; | ||
|
||
for await (const file of walk(`./CTS/clients/${client}`)) { | ||
if (!file.name.endsWith('json')) { | ||
continue; | ||
} | ||
const operationId = file.name.replace('.json', ''); | ||
const tests: CTSBlock[] = JSON.parse( | ||
(await fsp.readFile(file.path)).toString() | ||
); | ||
|
||
// check test validity against spec | ||
if (!operations.includes(operationId)) { | ||
throw new Error( | ||
`cannot find operationId ${operationId} for the ${client} client` | ||
); | ||
} | ||
|
||
// for now we stringify all params for mustache to render them properly | ||
for (const test of tests) { | ||
for (let i = 0; i < test.parameters.length; i++) { | ||
// delete the object name for now, but it could be use for `new $objectName(params)` | ||
delete test.parameters[i].$objectName; | ||
millotp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// include the `-last` param to join with comma in mustache | ||
test.parameters[i] = { | ||
value: JSON.stringify(test.parameters[i]), | ||
'-last': i === test.parameters.length - 1, | ||
}; | ||
} | ||
|
||
// stringify request.data too | ||
test.request.data = JSON.stringify(test.request.data); | ||
} | ||
ctsClient.push(...tests); | ||
} | ||
return ctsClient; | ||
} | ||
|
||
async function loadCTS(): Promise<void> { | ||
for await (const { name: client } of await fsp.opendir('./CTS/clients/')) { | ||
cts[client] = await loadCTSForClient(client); | ||
} | ||
} | ||
|
||
async function loadTemplate(language: Language): Promise<string> { | ||
return (await fsp.readFile(`CTS/templates/${language}.mustache`)).toString(); | ||
} | ||
|
||
async function generateCode(language: Language): Promise<void> { | ||
const template = await loadTemplate(language); | ||
await createOutputDir(language); | ||
for (const client in cts) { | ||
if (cts[client].length === 0) { | ||
continue; | ||
} | ||
|
||
const code = Mustache.render(template, { | ||
import: packageNames[language][client], | ||
client: `${capitalize(client)}Api`, | ||
tests: cts[client], | ||
}); | ||
await fsp.writeFile( | ||
`output/${language}/${client}${extensionForLanguage[language]}`, | ||
code | ||
); | ||
} | ||
} | ||
|
||
function printUsage(): void { | ||
console.log(`usage: generateCTS all | language1 language2...`); | ||
console.log(`\tavailable languages: ${availableLanguages.join(',')}`); | ||
// eslint-disable-next-line no-process-exit | ||
process.exit(1); | ||
} | ||
|
||
async function parseCLI(args: string[]): Promise<void> { | ||
if (args.length < 3) { | ||
console.log('not enough arguments'); | ||
printUsage(); | ||
} | ||
|
||
let toGenerate: Language[]; | ||
if (args.length === 3 && args[2] === 'all') { | ||
toGenerate = [...availableLanguages]; | ||
} else { | ||
const languages = args[2].split(' ') as Language[]; | ||
const unknownLanguages = languages.filter( | ||
(lang) => !availableLanguages.includes(lang) | ||
); | ||
if (unknownLanguages.length > 0) { | ||
console.log('unkown language(s): ', unknownLanguages.join(', ')); | ||
printUsage(); | ||
} | ||
toGenerate = languages; | ||
} | ||
|
||
try { | ||
await loadCTS(); | ||
for (const lang of toGenerate) { | ||
generateCode(lang); | ||
} | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
console.error(e); | ||
} | ||
} | ||
} | ||
|
||
parseCLI(process.argv); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
require('dotenv').config(); | ||
|
||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.