Skip to content

Commit

Permalink
feat(test): adds and cover the utility functions for testing actual a…
Browse files Browse the repository at this point in the history
…nd expected responses (#182)
  • Loading branch information
asadali214 authored Jul 22, 2024
1 parent ad18ad0 commit 2c1c60c
Show file tree
Hide file tree
Showing 13 changed files with 891 additions and 2 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: yarn build

- name: Test
run: yarn test --ci --coverage --maxWorkers=2
run: yarn test

- name: Lint
run: yarn lint
Expand All @@ -49,4 +49,5 @@ jobs:
${{github.workspace}}/packages/http-headers/coverage/lcov.info:lcov
${{github.workspace}}/packages/http-query/coverage/lcov.info:lcov
${{github.workspace}}/packages/oauth-adapters/coverage/lcov.info:lcov
${{github.workspace}}/packages/schema/coverage/lcov.info:lcov
${{github.workspace}}/packages/schema/coverage/lcov.info:lcov
${{github.workspace}}/packages/test-utilities/coverage/lcov.info:lcov
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ They provide common runtime utilities needed by SDKs to make API calls and handl
| [@apimatic/http-query](packages/http-query) | HTTP Query utilities for apimatic-js-runtime libraries |
| [@apimatic/oauth-adapters](packages/oauth-adapters) | Provides pluggable adapters for OAuth 2.0 authentication schemes. |
| [@apimatic/xml-adapter](packages/xml-adapter) | Provides XML serialization and deserialization utilities for apimatic-js-runtime libraries. |
| [@apimatic/test-utilities](packages/test-utilities) | Provides assertion utilities for testing api calls. It can be plugged in as dev dependency to any library. |

[ci-badge]: https://github.com/apimatic/apimatic-js-runtime/actions/workflows/main.yml/badge.svg
[ci-url]: https://github.com/apimatic/apimatic-js-runtime/actions/workflows/main.yml
Expand Down
35 changes: 35 additions & 0 deletions packages/test-utilities/README.md
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.
7 changes: 7 additions & 0 deletions packages/test-utilities/jest.config.js
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: '../../' }]]
};
66 changes: 66 additions & 0 deletions packages/test-utilities/package.json
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"
}
}
32 changes: 32 additions & 0 deletions packages/test-utilities/rollup.config.js
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'
}
]
})
];
124 changes: 124 additions & 0 deletions packages/test-utilities/src/assertionUtils.ts
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);
});
}
2 changes: 2 additions & 0 deletions packages/test-utilities/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './streamUtils';
export * from './assertionUtils';
65 changes: 65 additions & 0 deletions packages/test-utilities/src/streamUtils.ts
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);
}
Loading

0 comments on commit 2c1c60c

Please sign in to comment.