Skip to content
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

feat(test): adds and cover the utility functions for testing actual and expected responses #182

Merged
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
15b40fb
feat(utilities): add test helper utilities
Jun 27, 2024
01ac894
Merge branch 'master' of https://github.com/apimatic/apimatic-js-runt…
asadali214 Jun 28, 2024
90fd6f5
adds utility functions for headers matching and file retrieving
asadali214 Jun 28, 2024
c50784d
refactor: updates dependencies
asadali214 Jun 28, 2024
f32b9a0
feat: adds support for more types in requestBuilder.text()
asadali214 Jun 28, 2024
c718239
build: downgrade yarn lock file
asadali214 Jun 29, 2024
97ca2bc
refactor: check style fixes
asadali214 Jun 29, 2024
b3fd863
refactor: check style fix
asadali214 Jun 29, 2024
27f2845
feat: implement new utilities in test helper
Jul 9, 2024
7d09572
Merge branch 'master' into 177-add-test-helper-utilities-for-comparin…
asadali214 Jul 9, 2024
1760955
fix(schema): functionality of discriminated object
asadali214 Jul 10, 2024
77856b4
fix(schema): functionality of discriminated object with nullable oute…
asadali214 Jul 10, 2024
4ff7126
fix(typescript): add string enum handling
Jul 11, 2024
6b74313
Merge branch '177-add-test-helper-utilities-for-comparing-array-and-o…
asadali214 Jul 11, 2024
8c3eaf1
refactor: major refactoring of new requestBuilder functions
asadali214 Jul 11, 2024
4851877
fix: build issue in requestBuilder
asadali214 Jul 11, 2024
9dbf6ac
refactor(typescript): move test helper functionality in a new package
Jul 11, 2024
cd63db5
ci: updated pacakge version
asadali214 Jul 12, 2024
e510e1d
refactor(tests): refactores test-helper tests
Jul 12, 2024
fff67d5
Merge branch '177-add-test-helper-utilities-for-comparing-array-and-o…
Jul 12, 2024
4c4c3f9
fix: incorrect import issue
asadali214 Jul 12, 2024
7f9683a
refactor: formatting issues in testHelper.test
asadali214 Jul 12, 2024
02a39ae
fix: isSameAsFile function
asadali214 Jul 12, 2024
84a8669
fix: stream matching tests using mock http client
asadali214 Jul 14, 2024
5085c80
fix: stream fetching and matching
asadali214 Jul 15, 2024
833581f
feat: added new dependency information to all areas
asadali214 Jul 15, 2024
8acb66b
ci: capability to upload coverage report for test-helper
asadali214 Jul 15, 2024
4c19779
ci: remove axios from jest configuration of test-helper library
asadali214 Jul 15, 2024
baf65b8
build: merge master into current branch
asadali214 Jul 18, 2024
5653e73
docs: adds readme to test-helper package
asadali214 Jul 18, 2024
e226239
feat: renamed test-helper package and refactored utility functions
asadali214 Jul 21, 2024
c9841f3
ci: capability to upload coverage report for test-utilities
asadali214 Jul 21, 2024
896cad0
docs: apply new utilities changes to readme
asadali214 Jul 21, 2024
57204ec
Merge branch '177-add-test-helper-utilities-for-comparing-array-and-o…
asadali214 Jul 21, 2024
5ec731e
build: remove jest-extended dependency
asadali214 Jul 21, 2024
54880d5
refactor: refactored assertion function to reduce complexity
asadali214 Jul 21, 2024
cc41d16
fix: adds support for null and undefined matching
asadali214 Jul 21, 2024
64f6918
build: refactored rollup config
asadali214 Jul 21, 2024
095ed4f
refactor: streams matching function directly throws error now
asadali214 Jul 21, 2024
446bd9f
feat: adds a new utility to get buffer data
asadali214 Jul 22, 2024
e89af4d
refactor: rename test suite
asadali214 Jul 22, 2024
8dca2a4
refactor: removed unnecessary test
asadali214 Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-helper/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-helper](packages/test-helper) | 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
15 changes: 15 additions & 0 deletions packages/core-interfaces/src/httpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { HttpRequest, RequestOptions } from './httpRequest';
import { HttpResponse } from './httpResponse';

/**
* Interface defining the contract for an HTTP client.
* Implementations of this interface should handle making HTTP requests
* and returning promises that resolve to HTTP responses.
* @param request The HTTP request to be sent.
* @param requestOptions Optional additional options for the request.
* @returns A promise that resolves to an HTTP response.
*/
export type HttpClientInterface = (
request: HttpRequest,
requestOptions?: RequestOptions
) => Promise<HttpResponse>;
1 change: 1 addition & 0 deletions packages/core-interfaces/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './apiResponse';
export * from './authentication';
export * from './httpContext';
export * from './httpClient';
export * from './httpInterceptor';
export * from './httpRequest';
export * from './httpResponse';
Expand Down
94 changes: 46 additions & 48 deletions packages/core/src/http/requestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import {
HttpRequest,
HttpRequestMultipartFormBody,
HttpRequestUrlEncodedFormBody,
HttpResponse,
HttpInterceptorInterface,
RequestOptions,
RetryConfiguration,
ApiLoggerInterface,
HttpClientInterface,
} from '../coreInterfaces';
import { ArgumentsValidationError } from '../errors/argumentsValidationError';
import { ResponseValidationError } from '../errors/responseValidationError';
import {
Schema,
SchemaValidationError,
validateAndMap,
validateAndMapXml,
validateAndUnmapXml,
Expand Down Expand Up @@ -81,11 +82,6 @@ export function skipEncode<T extends PathTemplatePrimitiveTypes>(
return new SkipEncode(value);
}

export type HttpClientInterface = (
request: HttpRequest,
requestOptions?: RequestOptions
) => Promise<HttpResponse>;

export type ApiErrorConstructor = new (
response: HttpContext,
message: string
Expand Down Expand Up @@ -120,7 +116,7 @@ export interface RequestBuilder<BaseUrlParamType, AuthParams> {
headers(headersToMerge: Record<string, string>): void;
query(
name: string,
value: QueryValue,
value: QueryValue | Record<string, QueryValue>,
prefixFormat?: ArrayPrefixFunction
): void;
query(
Expand All @@ -135,7 +131,7 @@ export interface RequestBuilder<BaseUrlParamType, AuthParams> {
parameters: Record<string, unknown>,
prefixFormat?: ArrayPrefixFunction
): void;
text(body: string): void;
text(body: string | number | bigint | boolean | null | undefined): void;
json(data: unknown): void;
requestRetryOption(option: RequestRetryOption): void;
xml<T>(
Expand Down Expand Up @@ -289,7 +285,7 @@ export class DefaultRequestBuilder<BaseUrlParamType, AuthParams>
}
public query(
name: string,
value: QueryValue,
value: QueryValue | Record<string, QueryValue>,
prefixFormat?: ArrayPrefixFunction
): void;
public query(
Expand Down Expand Up @@ -317,8 +313,10 @@ export class DefaultRequestBuilder<BaseUrlParamType, AuthParams>
this._query.push(queryString);
}
}
public text(body: string): void {
this._body = body;
public text(
body: string | number | bigint | boolean | null | undefined
): void {
this._body = body?.toString() ?? undefined;
this._setContentTypeIfNotSet(TEXT_CONTENT_TYPE);
}
public json(data: unknown): void {
Expand Down Expand Up @@ -499,41 +497,9 @@ export class DefaultRequestBuilder<BaseUrlParamType, AuthParams>
return { ...request, headers };
});
const result = await this.call(requestOptions);
if (typeof result.body !== 'string') {
throw new Error(
'Could not parse body as JSON. The response body is not a string.'
);
}
if (result.body.trim() === '') {
// Try mapping the missing body as null
return this.tryMappingAsNull<T>(schema, result);
}
let parsed: unknown;
try {
parsed = JSON.parse(result.body);
} catch (error) {
throw new Error(`Could not parse body as JSON.\n\n${error.message}`);
}
const mappingResult = validateAndMap(parsed, schema);
if (mappingResult.errors) {
throw new ResponseValidationError(result, mappingResult.errors);
}
return { ...result, result: mappingResult.result };
}

private tryMappingAsNull<T>(
schema: Schema<T, any>,
result: ApiResponse<void>
) {
const nullMappingResult = validateAndMap(null, schema);
if (nullMappingResult.errors) {
throw new Error(
'Could not parse body as JSON. The response body is empty.'
);
}
return { ...result, result: nullMappingResult.result };
return { ...result, result: parseJsonResult(schema, result) };
}

public async callAsXml<T>(
rootName: string,
schema: Schema<T, any>,
Expand Down Expand Up @@ -593,7 +559,6 @@ export class DefaultRequestBuilder<BaseUrlParamType, AuthParams>
return context;
});
}

private _addApiLoggerInterceptors(): void {
if (this._apiLogger) {
const apiLogger = this._apiLogger;
Expand All @@ -606,14 +571,12 @@ export class DefaultRequestBuilder<BaseUrlParamType, AuthParams>
});
}
}

private _addAuthentication() {
this.intercept((...args) => {
const handler = this._authenticationProvider(this._authParams);
return handler(...args);
});
}

private _addRetryInterceptor() {
this.intercept(async (request, options, next) => {
let context: HttpContext | undefined;
Expand Down Expand Up @@ -659,7 +622,6 @@ export class DefaultRequestBuilder<BaseUrlParamType, AuthParams>
return { request, response: context.response };
});
}

private _addErrorHandlingInterceptor() {
this.interceptResponse((context) => {
const { response } = context;
Expand Down Expand Up @@ -720,3 +682,39 @@ function mergePath(left: string, right?: string): string {
return `${left}/${right}`;
}
}
function parseJsonResult<T>(schema: Schema<T, any>, res: ApiResponse<void>): T {
if (typeof res.body !== 'string') {
throw new Error(
'Could not parse body as JSON. The response body is not a string.'
);
}
if (res.body.trim() === '') {
const resEmptyErr = new Error(
'Could not parse body as JSON. The response body is empty.'
);
return validateJson(schema, null, (_) => resEmptyErr);
}
let parsed: unknown;
try {
parsed = JSON.parse(res.body);
} catch (error) {
const resUnParseErr = new Error(
`Could not parse body as JSON.\n\n${error.message}`
);
return validateJson(schema, res.body, (_) => resUnParseErr);
}
const resInvalidErr = (errors: SchemaValidationError[]) =>
new ResponseValidationError(res, errors);
return validateJson(schema, parsed, (errors) => resInvalidErr(errors));
}
function validateJson<T>(
schema: Schema<T, any>,
value: any,
errorCreater: (errors: SchemaValidationError[]) => Error
): T {
const mappingResult = validateAndMap(value, schema);
if (mappingResult.errors) {
throw errorCreater(mappingResult.errors);
}
return mappingResult.result;
}
2 changes: 1 addition & 1 deletion packages/core/test/http/requestBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
HttpClientInterface,
RequestBuilder,
createRequestBuilderFactory,
skipEncode,
} from '../../src/http/requestBuilder';
import {
AuthenticatorInterface,
HttpClientInterface,
HttpMethod,
HttpRequest,
HttpResponse,
Expand Down
51 changes: 35 additions & 16 deletions packages/schema/src/types/discriminatedObject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Schema, SchemaMappedType, SchemaType } from '../schema';
import {
Schema,
SchemaMappedType,
SchemaType,
SchemaValidationError,
} from '../schema';
import { objectEntries } from '../utils';
import { ObjectXmlOptions } from './object';

Expand All @@ -14,9 +19,11 @@ export function discriminatedObject<
defaultDiscriminator: keyof TDiscrimMap,
xmlOptions?: ObjectXmlOptions
): Schema<any, any> {
const schemaSelector = (
const allSchemas = Object.values(discriminatorMap).reverse();
const selectSchema = (
value: unknown,
discriminatorProp: string | TDiscrimProp | TDiscrimMappedProp,
checker: (schema: TSchema) => SchemaValidationError[],
isAttr: boolean = false
) => {
if (
Expand All @@ -39,41 +46,53 @@ export function discriminatedObject<
return discriminatorMap[discriminatorValue];
}
}
for (const key in allSchemas) {
if (checker(allSchemas[key]).length === 0) {
return allSchemas[key];
}
}
return discriminatorMap[defaultDiscriminator];
};

return {
type: () =>
`DiscriminatedUnion<${discriminatorPropName},[${objectEntries(
`DiscriminatedUnion<${discriminatorPropName as string},[${objectEntries(
discriminatorMap
)
.map(([_, v]) => v.type)
.join(',')}]>`,
map: (value, ctxt) =>
schemaSelector(value, discriminatorPropName).map(value, ctxt),
selectSchema(value, discriminatorPropName, (schema) =>
schema.validateBeforeMap(value, ctxt)
).map(value, ctxt),
unmap: (value, ctxt) =>
schemaSelector(value, discriminatorMappedPropName).unmap(value, ctxt),
selectSchema(value, discriminatorMappedPropName, (schema) =>
schema.validateBeforeUnmap(value, ctxt)
).unmap(value, ctxt),
validateBeforeMap: (value, ctxt) =>
schemaSelector(value, discriminatorPropName).validateBeforeMap(
value,
ctxt
),
selectSchema(value, discriminatorPropName, (schema) =>
schema.validateBeforeMap(value, ctxt)
).validateBeforeMap(value, ctxt),
validateBeforeUnmap: (value, ctxt) =>
schemaSelector(value, discriminatorMappedPropName).validateBeforeUnmap(
value,
ctxt
),
selectSchema(value, discriminatorMappedPropName, (schema) =>
schema.validateBeforeUnmap(value, ctxt)
).validateBeforeUnmap(value, ctxt),
mapXml: (value, ctxt) =>
schemaSelector(
selectSchema(
value,
xmlOptions?.xmlName ?? discriminatorPropName,
(schema) => schema.validateBeforeMapXml(value, ctxt),
xmlOptions?.isAttr
).mapXml(value, ctxt),
unmapXml: (value, ctxt) =>
schemaSelector(value, discriminatorMappedPropName).unmapXml(value, ctxt),
selectSchema(value, discriminatorMappedPropName, (schema) =>
schema.validateBeforeUnmap(value, ctxt)
).unmapXml(value, ctxt),
validateBeforeMapXml: (value, ctxt) =>
schemaSelector(
selectSchema(
value,
xmlOptions?.xmlName ?? discriminatorPropName,
(schema) => schema.validateBeforeMapXml(value, ctxt),
xmlOptions?.isAttr
).validateBeforeMapXml(value, ctxt),
};
Expand Down
5 changes: 4 additions & 1 deletion packages/schema/src/types/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,10 @@ function validateValueObject({
ctxt.createChild(propTypePrefix + key, valueObject[key], schema)
)
);
} else if (schema.type().indexOf('Optional<') !== 0) {
} else if (
!schema.type().startsWith('Optional<') &&
!schema.type().startsWith('Nullable<')
) {
// Add to missing keys if it is not an optional property
missingProps.add(key);
}
Expand Down
Loading
Loading