-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(test): adds and cover the utility functions for testing actual a…
…nd expected responses (#182)
- Loading branch information
1 parent
ad18ad0
commit 2c1c60c
Showing
13 changed files
with
891 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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 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 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,35 @@ | ||
# APIMatic Test Utilities Libary for JavaScript | ||
|
||
> This library is currently in preview. | ||
Provides assertion utilities for testing api calls. It can be plugged in as dev dependency to any library. | ||
|
||
The exported helper functions include: | ||
|
||
1. **getStreamData**: Get streaming data from a given URL. | ||
2. **toBuffer**: Promise to create a Buffer instance from a NodeJS.ReadableStream or Blob. | ||
3. **expectHeadersToMatch**: Compare actual headers with expected headers, ignoring case sensitivity. | ||
4. **expectMatchingWithOptions**: Check whether the expected value is matching with the actual value, with the given comparison options. | ||
|
||
This library is used as a dev-dependency by JavaScript SDKs generated by the [APIMatic Code Generator](http://www.apimatic.io). | ||
|
||
## Builds | ||
|
||
The following environments are supported: | ||
|
||
1. Node.js v10+ | ||
1. Bundlers like Rollup or Webpack | ||
1. Web browsers | ||
|
||
To support multiple environments, we export various builds: | ||
|
||
| Environment | Usage | | ||
| --- | --- | | ||
| Common.js | Import like this: `require('@apimatic/test-utilities')`. | | ||
| ES Module | Import like this: `import { /* your imports */ } from '@apimatic/test-utilities'`. | | ||
| Browsers | *Use script: `https://unpkg.com/@apimatic/test-utilities@VERSION/umd/schema.js` | | ||
| Modern Browsers (supports ESM and uses modern JS) | *Use script: `https://unpkg.com/@apimatic/test-utilities@VERSION/umd/schema.esm.js` | | ||
|
||
_* Don't forget to replace VERSION with the version number._ | ||
|
||
**Note**: We discourage importing files or modules directly from the package. These are likely to change in the future and should not be considered stable. |
This file contains 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 @@ | ||
const { jest: lernaAliases } = require('lerna-alias'); | ||
|
||
module.exports = { | ||
preset: 'ts-jest', | ||
moduleNameMapper: lernaAliases(), | ||
coverageReporters: [['lcov', { projectRoot: '../../' }]] | ||
}; |
This file contains 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,66 @@ | ||
{ | ||
"name": "@apimatic/test-utilities", | ||
"version": "0.0.0", | ||
"description": "provides the assertion utilities", | ||
"main": "lib/index.js", | ||
"module": "lib/index.js", | ||
"types": "lib/index.d.ts", | ||
"engines": { | ||
"node": ">=14.15.0 || >=16.0.0" | ||
}, | ||
"scripts": { | ||
"clean": "rm -rf lib es umd tsconfig.tsbuildinfo", | ||
"test": "jest --passWithNoTests", | ||
"build": "npm run clean && tsc && rollup -c && npm run annotate:es", | ||
"annotate:es": "babel es --out-dir es --no-babelrc --plugins annotate-pure-calls", | ||
"preversion": "npm run test", | ||
"prepublishOnly": "npm run build", | ||
"size": "size-limit", | ||
"analyze": "size-limit --why", | ||
"lint": "tslint --project .", | ||
"lint:fix": "tslint --project . --fix", | ||
"check-style": "prettier --check \"{src,test}/**/*.ts\"", | ||
"check-style:fix": "prettier --write \"{src,test}/**/*.ts\"" | ||
}, | ||
"author": "APIMatic Ltd.", | ||
"license": "ISC", | ||
"size-limit": [ | ||
{ | ||
"path": "umd/schema.js", | ||
"limit": "5 KB" | ||
}, | ||
{ | ||
"path": "umd/schema.esm.js", | ||
"limit": "5 KB" | ||
} | ||
], | ||
"devDependencies": { | ||
"@babel/cli": "^7.21.5", | ||
"@babel/core": "^7.22.1", | ||
"@rollup/plugin-terser": "^0.4.3", | ||
"@size-limit/preset-small-lib": "^7.0.8", | ||
"babel-plugin-annotate-pure-calls": "^0.4.0", | ||
"jest": "^26.4.2", | ||
"jsdom": "^19.0.0", | ||
"jsdom-global": "^3.0.2", | ||
"lerna-alias": "3.0.3-0", | ||
"rollup": "^2.79.0", | ||
"rollup-plugin-replace": "^2.2.0", | ||
"rollup-plugin-typescript2": "^0.31.0", | ||
"size-limit": "^7.0.8", | ||
"ts-jest": "^26.4.0", | ||
"typescript": "^4.1.2" | ||
}, | ||
"dependencies": { | ||
"@apimatic/core-interfaces": "^0.2.5", | ||
"tslib": "^2.1.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "[email protected]:apimatic/apimatic-js-runtime.git", | ||
"directory": "packages/test-utilities" | ||
} | ||
} |
This file contains 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,32 @@ | ||
import typescript from 'rollup-plugin-typescript2'; | ||
|
||
const getTsPlugin = ({ declaration = true, target } = {}) => | ||
typescript({ | ||
clean: true, | ||
tsconfigOverride: { | ||
compilerOptions: { | ||
declaration, | ||
...(target && { target }) | ||
} | ||
} | ||
}); | ||
|
||
const getNpmConfig = ({ input, output, external }) => ({ | ||
input, | ||
output, | ||
preserveModules: true, | ||
plugins: [getTsPlugin({ declaration: true })], | ||
external | ||
}); | ||
|
||
export default [ | ||
getNpmConfig({ | ||
input: 'src/index.ts', | ||
output: [ | ||
{ | ||
dir: 'es', | ||
format: 'esm' | ||
} | ||
] | ||
}) | ||
]; |
This file contains 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,124 @@ | ||
/** | ||
* Interface defining options for subset comparison functions. | ||
*/ | ||
export interface ExpectOptions { | ||
/** | ||
* Whether extra elements are allowed in the actual object or array. | ||
* Default: false | ||
*/ | ||
allowExtra?: boolean; | ||
|
||
/** | ||
* Note: only applicable to objects (always true for arrays) | ||
* Whether to check primitive values for equality. | ||
* Default: false | ||
*/ | ||
checkValues?: boolean; | ||
|
||
/** | ||
* Note: only applicable to arrays (always false for objects) | ||
* Whether elements in the actual array should be compared in order to the expected array. | ||
* Default: false | ||
*/ | ||
isOrdered?: boolean; | ||
} | ||
|
||
/** | ||
* Compare actual headers with expected headers, ignoring case sensitivity. | ||
* @param actualHeaders Actual headers received from the request. | ||
* @param expectedHeaders Expected headers with values to match against actual headers. | ||
*/ | ||
export function expectHeadersToMatch( | ||
actualHeaders: Record<string, string>, | ||
expectedHeaders: Record<string, Array<string | boolean>> | ||
): void { | ||
const lowerCasedHeaders = Object.keys(actualHeaders).reduce((acc, key) => { | ||
acc[key.toLowerCase()] = actualHeaders[key]; | ||
return acc; | ||
}, {} as Record<string, string>); | ||
|
||
Object.entries(expectedHeaders).forEach(([expectedKey, expectedValue]) => { | ||
const lowerCasedKey = expectedKey.toLowerCase(); | ||
expect(lowerCasedHeaders).toHaveProperty(lowerCasedKey); | ||
if (expectedValue[1]) { | ||
expect(lowerCasedHeaders[lowerCasedKey]).toBe(expectedValue[0]); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Check whether the expected value is matching with the actual value. | ||
* @param expected Expected value. | ||
* @param actual Actual value to be matched with expected value. | ||
* @param options Options for comparison of actual value with expected value. | ||
*/ | ||
export function expectMatchingWithOptions( | ||
expected: any, | ||
actual?: any, | ||
options: ExpectOptions = {} | ||
): void { | ||
expect(typeof actual).toEqual(typeof expected); | ||
|
||
const { | ||
isOrdered = false, | ||
checkValues = false, | ||
allowExtra = false, | ||
} = options; | ||
|
||
checkIfMatching(expected, actual, isOrdered, checkValues); | ||
// when extra values are not allowed in actual array or object, | ||
// check by inverting actual and expected values. | ||
allowExtra || checkIfMatching(actual, expected, isOrdered, checkValues); | ||
} | ||
|
||
/** | ||
* Recursively checks if right object or array contains all the elements | ||
* of left object or array. | ||
* @param left Left value. | ||
* @param right Right value to be matched with left value. | ||
* @param isOrdered Whether to check order of elements in arrays. | ||
* @param checkValues Whether to check values of each key in objects. | ||
*/ | ||
function checkIfMatching( | ||
left: any, | ||
right: any, | ||
isOrdered: boolean, | ||
checkValues: boolean | ||
): void { | ||
function isObject(value: any): value is object { | ||
return value !== null && typeof value === 'object'; | ||
} | ||
|
||
if (Array.isArray(left) && Array.isArray(right)) { | ||
checkArrays(left, right, isOrdered); | ||
} else if (isObject(left) && isObject(right)) { | ||
checkObjects(left, right, isOrdered, checkValues); | ||
} else if (checkValues) { | ||
expect(left).toEqual(right); | ||
} | ||
} | ||
|
||
function checkArrays(left: any[], right: any[], isOrdered: boolean) { | ||
if (isOrdered) { | ||
// Check if right array is directly equal to a partial left array. | ||
expect(right).toEqual(expect.objectContaining(left)); | ||
return; | ||
} | ||
// Or check if right array contains all elements from left array. | ||
left.forEach((leftVal) => expect(right).toContainEqual(leftVal)); | ||
} | ||
|
||
function checkObjects( | ||
left: object, | ||
right: object, | ||
isOrdered: boolean, | ||
checkValues: boolean | ||
) { | ||
const rightObjKeys = Object.keys(right); | ||
Object.keys(left).forEach((key) => { | ||
// Check if right object keys contains this key from left object. | ||
expect(rightObjKeys).toContainEqual(key); | ||
// Recursive checking for each element in left and right object. | ||
checkIfMatching(left[key], right[key], isOrdered, checkValues); | ||
}); | ||
} |
This file contains 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,2 @@ | ||
export * from './streamUtils'; | ||
export * from './assertionUtils'; |
This file contains 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,65 @@ | ||
import { HttpClientInterface } from '@apimatic/core-interfaces'; | ||
import { Readable } from 'stream'; | ||
|
||
/** | ||
* Get streaming data from a given URL. | ||
* @param client Instance of HttpClient to be used. | ||
* @param url URL from which to create the readable stream. | ||
* @returns Stream of data fetched from the URL. | ||
* @throws Error if unable to retrieve data from the URL. | ||
*/ | ||
export async function getStreamData( | ||
client: HttpClientInterface, | ||
url: string | ||
): Promise<NodeJS.ReadableStream | Blob> { | ||
const res = await client({ method: 'GET', url, responseType: 'stream' }); | ||
if (res.statusCode !== 200 || typeof res.body === 'string') { | ||
throw new Error(`Unable to retrieve streaming data from ${url}`); | ||
} | ||
return res.body; | ||
} | ||
|
||
/** | ||
* Convert a NodeJS ReadableStream or Blob to a Buffer. | ||
* @param input NodeJS ReadableStream or Blob to convert. | ||
* @returns Promise resolving to a Buffer containing the input data. | ||
*/ | ||
export async function toBuffer( | ||
input: NodeJS.ReadableStream | Blob | undefined | ||
): Promise<Buffer> { | ||
if (typeof Blob !== 'undefined' && input instanceof Blob) { | ||
return blobToBuffer(input); | ||
} | ||
if (typeof Readable !== 'undefined' && input instanceof Readable) { | ||
return streamToBuffer(input); | ||
} | ||
throw new Error('Unsupported input type. Expected a Blob or ReadableStream.'); | ||
} | ||
|
||
/** | ||
* Convert a NodeJS ReadableStream to a Buffer. | ||
* @param stream Readable stream to convert. | ||
* @returns Promise resolving to a Buffer containing stream data. | ||
*/ | ||
async function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> { | ||
const chunks: Uint8Array[] = []; | ||
for await (const chunk of stream) { | ||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | ||
} | ||
return Buffer.concat(chunks); | ||
} | ||
|
||
/** | ||
* Convert a Blob to a Buffer. | ||
* @param blob Blob to convert. | ||
* @returns Promise resolving to an Buffer containing blob data. | ||
*/ | ||
async function blobToBuffer(blob: Blob): Promise<Buffer> { | ||
const arrayBuffer = new Promise<ArrayBuffer>((resolve, reject) => { | ||
const reader = new FileReader(); | ||
reader.onload = () => resolve(reader.result as ArrayBuffer); | ||
reader.onerror = reject; | ||
reader.readAsArrayBuffer(blob); | ||
}); | ||
return Buffer.from(await arrayBuffer); | ||
} |
Oops, something went wrong.