imp(testing): PR fixes
MODIFY
packages/core/src/+internal/testing/metadata.helper.ts
packages/core/src/+internal/testing/metadata.helper.ts
@@ -2,7 +2,7 @@ import { HttpHeaders, HttpRequest, HttpResponse } from '../../http.interface';
import { JSONSchema7 } from 'json-schema';
export interface TestingMetadata {
- path: string;
+ path?: string;
body?: JSONSchema7;
headers?: JSONSchema7;
params?: JSONSchema7;
@@ -28,5 +28,5 @@ export const applyMetadata = (req: HttpRequest, res: HttpResponse) => {
return;
}
- setMetadataHeader(res, req.meta as any);
+ setMetadataHeader(res, { ...req.meta });
};

MODIFY
packages/core/src/http.interface.ts
packages/core/src/http.interface.ts
@@ -12,7 +12,7 @@ export interface HttpRequest<
body: TBody;
params: TParams;
query: TQuery;
- meta: Record<string, any>;
+ meta?: Record<string, any>;
[key: string]: any;
}

MODIFY
packages/middleware-io/src/io.json-schema.ts
packages/middleware-io/src/io.json-schema.ts
@@ -161,6 +161,17 @@ const filterTree = <T>(element: T, matcher: (value: any) => boolean): T | undefi
return matcher(element) ? element : undefined;
};
+export const mergeJsonObjects = (...schemas: (JSONSchema7 | undefined)[]): JSONSchema7 | undefined => {
+ const filteredSchemas = schemas.filter(Boolean) as JSONSchema7[];
+ const result: JSONSchema7 = filteredSchemas[0];
+ for (const schema of filteredSchemas.slice(1)) {
+ const { properties, required } = schema;
+ result.properties = { ...result.properties, ...properties };
+ result.required = [...(result.required || []), ...(required || [])];
+ }
+ return result;
+};
+
export const ioTypeToJsonSchema = (ioType: t.Any | undefined): JSONSchema7 | undefined => {
if (!ioType) {
return undefined;

MODIFY
packages/middleware-io/src/io.request.middleware.ts
packages/middleware-io/src/io.request.middleware.ts
-import { HttpRequest, HttpError, HttpStatus } from '@marblejs/core';
-import { Observable, forkJoin, of, throwError } from 'rxjs';
-import { mergeMap, map, catchError, tap } from 'rxjs/operators';
-import { Schema, ValidatorOptions, validator$ } from './io.middleware';
+import { HttpError, HttpRequest, HttpStatus } from '@marblejs/core';
+import { forkJoin, Observable, of, throwError } from 'rxjs';
+import { catchError, map, mergeMap, tap } from 'rxjs/operators';
+import { Schema, validator$, ValidatorOptions } from './io.middleware';
import { IOError } from './io.error';
import { isTestingMetadataOn } from '@marblejs/core/dist/+internal/testing';
-import { ioTypeToJsonSchema } from './io.json-schema';
+import { ioTypeToJsonSchema, mergeJsonObjects } from './io.json-schema';
interface RequestSchema<TBody extends Schema, TParams extends Schema, TQuery extends Schema> {
body: TBody;
@@ -27,16 +27,23 @@ export const requestValidator$ = <TBody extends Schema, TParams extends Schema,
const queryValidator$ = validator$(schema.query, { ...options, context: Context.QUERY });
const headersValidator$ = validator$(schema.headers, { ...options, context: Context.HEADERS });
- return (req$: Observable<HttpRequest>) =>
- req$.pipe(
+ const addMetadata = (req: HttpRequest, name: keyof typeof schema) => {
+ req.meta = {
+ ...req.meta,
+ [name]: mergeJsonObjects(
+ req.meta && req.meta[name],
+ ioTypeToJsonSchema(schema[name]),
+ ),
+ };
+ };
+
+ return (req$: Observable<HttpRequest>) => req$.pipe(
tap(req => {
if (isTestingMetadataOn()) {
- Object.assign(req.meta, {
- body: ioTypeToJsonSchema(schema.body),
- params: ioTypeToJsonSchema(schema.params),
- query: ioTypeToJsonSchema(schema.query),
- headers: ioTypeToJsonSchema(schema.headers),
- });
+ addMetadata(req, 'body');
+ addMetadata(req, 'params');
+ addMetadata(req, 'query');
+ addMetadata(req, 'headers');
}
}),
mergeMap(req =>

MODIFY
packages/middleware-io/src/specs/io.json-schema.spec.ts
packages/middleware-io/src/specs/io.json-schema.spec.ts
import * as t from 'io-ts';
-import { ioTypeToJsonSchema, withJsonSchema } from '../io.json-schema';
+import { ioTypeToJsonSchema, mergeJsonObjects, withJsonSchema } from '../io.json-schema';
import { JSONSchema7 } from 'json-schema';
const customIntegerStringType = () => new t.Type<number, string, unknown>(
@@ -18,37 +18,37 @@ const customIntegerStringSchema: JSONSchema7 = {
};
describe('#ioTypeToJsonSchema', () => {
- it('supports t.string conversion', () => {
+ test('supports t.string conversion', () => {
expect(ioTypeToJsonSchema(t.string)).toEqual({ type: 'string' });
});
- it('supports t.number conversion', () => {
+ test('supports t.number conversion', () => {
expect(ioTypeToJsonSchema(t.number)).toEqual({ type: 'number' });
});
- it('supports t.int conversion', () => {
+ test('supports t.int conversion', () => {
expect(ioTypeToJsonSchema(t.Int)).toEqual({ type: 'integer' });
});
- it('supports t.boolean conversion', () => {
+ test('supports t.boolean conversion', () => {
expect(ioTypeToJsonSchema(t.boolean)).toEqual({ type: 'boolean' });
});
- it('supports t.array conversion', () => {
+ test('supports t.array conversion', () => {
expect(ioTypeToJsonSchema(t.array(t.string))).toEqual({
type: 'array',
items: { type: 'string' },
});
});
- it('supports t.record conversion', () => {
+ test('supports t.record conversion', () => {
expect(ioTypeToJsonSchema(t.record(t.string, t.number))).toEqual({
type: 'object',
additionalProperties: { type: 'number' },
});
});
- it('supports t.keyof conversion', () => {
+ test('supports t.keyof conversion', () => {
expect(ioTypeToJsonSchema(t.keyof({
a: null,
b: null,
@@ -58,7 +58,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.undefined conversion', () => {
+ test('supports t.undefined conversion', () => {
expect(ioTypeToJsonSchema(t.type({
requiredProp: t.string,
optionalProp: t.union([t.undefined, t.string]),
@@ -73,7 +73,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.interface conversion', () => {
+ test('supports t.interface conversion', () => {
expect(ioTypeToJsonSchema(t.interface({
name: t.string,
age: t.number,
@@ -88,7 +88,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.exact(t.interface) conversion', () => {
+ test('supports t.exact(t.interface) conversion', () => {
expect(ioTypeToJsonSchema(t.exact(t.interface({
name: t.string,
age: t.number,
@@ -103,7 +103,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.partial conversion', () => {
+ test('supports t.partial conversion', () => {
expect(ioTypeToJsonSchema(t.partial({
name: t.string,
age: t.number,
@@ -117,17 +117,17 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.literal conversion', () => {
+ test('supports t.literal conversion', () => {
expect(ioTypeToJsonSchema(t.literal(30))).toEqual({ const: 30 });
expect(ioTypeToJsonSchema(t.literal(true))).toEqual({ const: true });
expect(ioTypeToJsonSchema(t.literal('test'))).toEqual({ const: 'test' });
});
- it('supports t.null conversion', () => {
+ test('supports t.null conversion', () => {
expect(ioTypeToJsonSchema(t.null)).toEqual({ type: 'null' });
});
- it('supports t.tuple conversion', () => {
+ test('supports t.tuple conversion', () => {
expect(ioTypeToJsonSchema(t.tuple([
t.string, t.number, t.boolean,
]))).toEqual({
@@ -138,7 +138,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.union conversion', () => {
+ test('supports t.union conversion', () => {
expect(ioTypeToJsonSchema(t.union([
t.string, t.number, t.boolean,
]))).toEqual({
@@ -150,7 +150,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports optimized t.union of t.literal conversion', () => {
+ test('supports optimized t.union of t.literal conversion', () => {
expect(ioTypeToJsonSchema(t.union([
t.literal('abc'), t.literal('def'), t.literal('ghi'),
]))).toEqual({
@@ -158,7 +158,7 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports t.intersection conversion', () => {
+ test('supports t.intersection conversion', () => {
expect(ioTypeToJsonSchema(t.intersection([
t.interface({
a: t.number,
@@ -174,16 +174,16 @@ describe('#ioTypeToJsonSchema', () => {
});
});
- it('supports custom JsonSchema added to custom type', () => {
+ test('supports custom JsonSchema added to custom type', () => {
const integerString = withJsonSchema(customIntegerStringType(), customIntegerStringSchema);
expect(ioTypeToJsonSchema(integerString)).toEqual(customIntegerStringSchema);
});
- it('returns undefined if passed type is undefined', () => {
+ test('returns undefined if passed type is undefined', () => {
expect(ioTypeToJsonSchema(undefined)).toBe(undefined);
});
- it('throws an error on unknown io-ts type', () => {
+ test('throws an error on unknown io-ts type', () => {
expect(() => ioTypeToJsonSchema({
_tag: 'this is not a valid type'
} as any)).toThrow('Unsupported type');
@@ -191,7 +191,7 @@ describe('#ioTypeToJsonSchema', () => {
});
describe('#withJsonSchema', () => {
- it('adds custom metadata to io-ts type', () => {
+ test('adds custom metadata to io-ts type', () => {
const integerString = customIntegerStringType();
expect((integerString as any)._tag).toBeUndefined();
@@ -201,3 +201,51 @@ describe('#withJsonSchema', () => {
expect(integerString).toHaveProperty('jsonSchema', customIntegerStringSchema);
});
});
+
+describe('#mergeJsonObjects', () => {
+ const object1: JSONSchema7 = {
+ type: 'object',
+ properties: {
+ test: { type: 'string' },
+ test2: { type: 'number' },
+ },
+ required: ['test2'],
+ };
+
+ const object2: JSONSchema7 = {
+ type: 'object',
+ properties: {
+ test3: { type: 'boolean' },
+ test4: { type: 'boolean' },
+ },
+ required: ['test3'],
+ };
+
+ test('merges properties and requires of objects', () => {
+ expect(mergeJsonObjects(
+ object1,
+ object2,
+ )).toEqual({
+ type: 'object',
+ properties: {
+ test: { type: 'string' },
+ test2: { type: 'number' },
+ test3: { type: 'boolean' },
+ test4: { type: 'boolean' },
+ },
+ required: ['test2', 'test3'],
+ });
+ });
+ test('handles undefined objects', () => {
+ expect(mergeJsonObjects(undefined, undefined)).toBeUndefined();
+ });
+ test('handles mixed values', () => {
+ expect(mergeJsonObjects(
+ undefined,
+ { type: 'object' },
+ object1,
+ { type: 'object' },
+ undefined,
+ )).toEqual(object1);
+ });
+});

MODIFY
packages/testing/src/apiEndpoint.helper.ts
packages/testing/src/apiEndpoint.helper.ts
@@ -64,7 +64,7 @@ export const generateResponse = (
.filter(header => {
debugger;
return !generationOptions.skippedResponseHeaders
- .find(h => h.toLowerCase() === header.toLowerCase())
+ .find(h => h.toLowerCase() === header.toLowerCase());
})
.reduce((result, key) => {
const value = getHeaderByKey(headers, key);

MODIFY
packages/testing/src/apiRequest.ts
packages/testing/src/apiRequest.ts
@@ -7,6 +7,9 @@ export type Bodyless = true;
export class ApiRequest<IsBodyless = false> {
public readonly request: TestRequest;
+ public readonly expectations: {
+ status?: number;
+ } = {};
constructor(private proxy: TestProxy, method: HttpMethod, path: string) {
this.request = {
@@ -41,7 +44,17 @@ export class ApiRequest<IsBodyless = false> {
return this as any;
}
+ expect(statusCode: number): this {
+ this.expectations.status = statusCode;
+ return this;
+ }
+
async send() {
- return new ApiResponse(this.request, await this.proxy.handle(this.request));
+ const { status } = this.expectations;
+ const response = new ApiResponse(this.request, await this.proxy.handle(this.request));
+ if (status && status !== response.status) {
+ throw new Error(`Unexpected status code ${response.status} in API Response. Expected ${status}.`);
+ }
+ return response;
}
}

MODIFY
packages/testing/src/apiResponse.ts
packages/testing/src/apiResponse.ts
@@ -58,13 +58,6 @@ export class ApiResponse {
};
}
- expectStatus(statusCode: number): this {
- if(this.status!==statusCode){
- throw new Error(`Unexpected status code ${this.status} in API Response. Expected ${statusCode}.`);
- }
- return this;
- }
-
collect(apiCollection: ApiCollection): this {
apiCollection.add(this);
return this;

MODIFY
packages/testing/src/spec/testing.spec.ts
packages/testing/src/spec/testing.spec.ts
@@ -28,7 +28,7 @@ describe('@marblejs/testing - TestApi', () => {
expect(response.failed).toBe(false);
expect(response.succeeded).toBe(true);
expect(response.status).toBe(HttpStatus.OK);
- expect(response.responseBody).toEqual({
+ expect(response.body).toEqual({
firstName: 'Cathy',
lastName: 'Scott',
email: 'cathy.scott98@example.com',
@@ -36,7 +36,7 @@ describe('@marblejs/testing - TestApi', () => {
address: '2059 W Craig Rd',
phone: '(700)-354-2326',
});
- expect(response.responseHeaders).toMatchObject({
+ expect(response.headers).toMatchObject({
'content-type': ['application/json'],
});

imp(testing): PR fixes