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

WIP: [refactor] updated test file structure #29

Merged
merged 11 commits into from
Mar 9, 2021
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,3 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# storybook
storybook-static
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,13 @@ export default createHandler(User);

| | Description |
| ----------------------- | ------------------------------------------- |
| `@Req()` | Gets the request object. |
| `@Res()`* | Gets the response object. |
| `@Body()` | Gets the request body. |
| `@Query(key: string)` | Gets a query string parameter value by key. |
| `@Header(name: string)` | Gets a header value by name. |

\* When using `@Res()`, you are in charge of sending the response to the client. Therefore, the return statement won't be handled by this package and the response won't be served.

## Built-in pipes

Expand All @@ -151,13 +154,12 @@ Pipes are being used to validate and transform incoming values. The pipes can be

⚠️ Beware that they throw when the value is invalid.

| | Description | Remarks |
| ------------------ | ------------------------------------------- | ------------------------------------------------- |
| `ParseBooleanPipe` | Validates and transforms `Boolean` strings. | Allows `'true'` and `'false'` as valid values |
| `ParseDatePipe` | Validates and transforms `Date` strings. | Allows valid `ISO 8601` formatted date strings |
| `ParseEnumPipe` | Validates and transforms `Enum` strings. | Allows strings that are present in the given enum |
| `ParseNumberPipe` | Validates and transforms `Number` strings. | Uses `parseFloat` under the hood |

| | Description | Remarks |
| ------------------ | ------------------------------------------- | -------------------------------------------------- |
| `ParseBooleanPipe` | Validates and transforms `Boolean` strings. | Allows `'true'` and `'false'` as valid values. |
| `ParseDatePipe` | Validates and transforms `Date` strings. | Allows valid `ISO 8601` formatted date strings. |
| `ParseNumberPipe` | Validates and transforms `Number` strings. | Uses `parseFloat` under the hood. |
| `ValidateEnumPipe` | Validates string based on `Enum` values. | Allows strings that are present in the given enum. |

## Exceptions

Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"rules": {
"@typescript-eslint/ban-types": "off"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'reflect-metadata';
import { HttpCode, HTTP_CODE_TOKEN } from '../../lib/decorators';
import { HttpCode, HTTP_CODE_TOKEN } from './httpCode.decorator';

class Test {
@HttpCode(201)
// eslint-disable-next-line @typescript-eslint/no-empty-function
public create(): void {}
}

it('HttpCode decorator should be set.', () =>
it('Should set the HttpCode decorator.', () =>
expect(Reflect.getMetadata(HTTP_CODE_TOKEN, Test, 'create')).toStrictEqual(201));
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'reflect-metadata';
import { Delete, Get, HTTP_METHOD_TOKEN, HttpVerb, Post, Put } from '../../lib/decorators';
import { Delete, Get, HTTP_METHOD_TOKEN, HttpVerb, Post, Put } from './httpMethod.decorators';

class Test {
@Get()
Expand All @@ -19,7 +19,7 @@ class Test {
public delete(): void {}
}

it('HttpMethod decorator should be set.', () => {
it('Should set the HttpMethod decorator.', () => {
const meta = Reflect.getMetadata(HTTP_METHOD_TOKEN, Test);
expect(meta).toBeInstanceOf(Map);
expect(meta).toMatchObject(
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/httpMethod.decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function applyHttpMethod(verb: HttpVerb) {

Reflect.defineMetadata(HTTP_METHOD_TOKEN, methods, target.constructor);

Handler(verb)(target, propertyKey, descriptor);
Handler()(target, propertyKey, descriptor);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import 'reflect-metadata';
import type { NextApiRequest, NextApiResponse } from 'next';
import { Body, PARAMETER_TOKEN, Req, Request, Res, Response, Header, Query } from '../../lib/decorators';
import { Body, PARAMETER_TOKEN, Req, Request, Res, Response, Header, Query } from './parameter.decorators';

describe('Parameter decorators', () => {
it('Body should be set.', () => {
it('Should set the Body decorator.', () => {
class Test {
public index(@Body() body: any) {}
}
Expand All @@ -16,7 +16,7 @@ describe('Parameter decorators', () => {
expect(meta).toMatchObject(expect.arrayContaining([expect.objectContaining({ index: 0, location: 'body' })]));
});

it('Header should be set.', () => {
it('Should set the Header decorator for the given names.', () => {
class Test {
public index(@Header('Content-Type') contentType: string, @Header('Referer') referer: string): void {}
}
Expand All @@ -32,7 +32,7 @@ describe('Parameter decorators', () => {
);
});

it('Query should be set for the whole query string.', () => {
it('Should set the Query decorator for the whole query string.', () => {
class Test {
public index(@Query() query: any) {}
}
Expand All @@ -45,7 +45,7 @@ describe('Parameter decorators', () => {
);
});

it('Query parameters should be set.', () => {
it('Should set the Query decorator for the given keys.', () => {
class Test {
public index(
@Query('firstName') firstName: string,
Expand All @@ -66,7 +66,7 @@ describe('Parameter decorators', () => {
);
});

it('Req should be set.', () => {
it('Should set the Req decorator.', () => {
class Test {
public index(@Req() req: NextApiRequest) {}
}
Expand All @@ -77,7 +77,7 @@ describe('Parameter decorators', () => {
expect(meta).toMatchObject(expect.arrayContaining([expect.objectContaining({ index: 0, location: 'request' })]));
});

it('Res should be set.', () => {
it('Should set the Res decorator.', () => {
class Test {
public index(@Res() res: NextApiResponse) {}
}
Expand All @@ -88,7 +88,7 @@ describe('Parameter decorators', () => {
expect(meta).toMatchObject(expect.arrayContaining([expect.objectContaining({ index: 0, location: 'response' })]));
});

it('Request and Response should be set.', () => {
it('Should set the Request and Response decoractors (aliases).', () => {
class Test {
public index(@Request() req: NextApiRequest, @Response() res: NextApiResponse) {}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import 'reflect-metadata';
import { HEADER_TOKEN, SetHeader } from '../../lib/decorators';
import { HEADER_TOKEN, SetHeader } from './setHeader.decorator';

@SetHeader('X-Api', 'true')
class Test {
@SetHeader('X-Method', 'index')
public index(): void {}
}

it('SetHeader should be set.', () => {
it('Should set the SetHeader decorator for the given name.', () => {
const meta = Reflect.getMetadata(HEADER_TOKEN, Test);
const methodMeta = Reflect.getMetadata(HEADER_TOKEN, Test, 'index');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
NotFoundException,
UnauthorizedException,
UnprocessableEntityException
} from '../../lib/exceptions';
} from '.';

describe('HttpException', () => {
it(`Should use 'HttpException' as name`, () =>
Expand All @@ -30,7 +30,7 @@ describe('HttpException', () => {
errors: ['Invalid email address']
}));

describe('Common errors', () => {
describe('Default errors', () => {
it('Should set the default status codes', () => {
expect(new BadRequestException()).toHaveProperty('statusCode', 400);
expect(new InternalServerErrorException()).toHaveProperty('statusCode', 500);
Expand Down
60 changes: 60 additions & 0 deletions lib/internals/classValidator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'reflect-metadata';
import { Expose } from 'class-transformer';
import { IsNotEmpty } from 'class-validator';
import { validateObject } from './classValidator';
import * as lp from './loadPackage';

describe('validateObject', () => {
it('Should return the value if "class-validator" is not being used.', async () => {
const spy = jest
.spyOn(lp, 'loadPackage')
.mockImplementation((name: string) => (name === 'class-validator' ? false : require(name)));

class Dto {
@IsNotEmpty()
public email!: string;
}

const result = await validateObject(Dto, { secondaryEmail: '[email protected]' });

expect(result).toHaveProperty('secondaryEmail', '[email protected]');

spy.mockRestore();
});

it('Should return the value if "class-transformer" is not being used.', async () => {
const spy = jest
.spyOn(lp, 'loadPackage')
.mockImplementation((name: string) => (name === 'class-transformer' ? false : require(name)));

class Dto {
@IsNotEmpty()
public email!: string;
}

const result = await validateObject(Dto, { secondaryEmail: '[email protected]' });

expect(result).toHaveProperty('secondaryEmail', '[email protected]');

spy.mockRestore();
});

it('Should return only exposed properties.', async () => {
class Dto {
@Expose()
@IsNotEmpty()
public email!: string;
}

const result = await validateObject(
Dto,
{ email: '[email protected]', secondaryEmail: '[email protected]' },
{
transformOptions: { excludeExtraneousValues: true }
}
);

expect(result).toHaveProperty('email', '[email protected]');
expect(result).not.toHaveProperty('secondaryEmail', '[email protected]');
});
});
12 changes: 1 addition & 11 deletions lib/internals/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ function getParameterValue(
return req.body;
case 'header':
return name ? req.headers[name.toLowerCase()] : req.headers;
case 'method':
return req.method;
case 'request':
return req;
case 'response':
Expand All @@ -29,18 +27,10 @@ function getParameterValue(
}
}

export function Handler(method?: HttpVerb): MethodDecorator {
if (!method) {
method = HttpVerb.GET;
}

export function Handler(): MethodDecorator {
return function (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) {
const originalHandler = descriptor.value;
descriptor.value = async function (req: NextApiRequest, res: NextApiResponse) {
if (req.method !== method) {
return notFound(req, res);
}

const httpCode: number | undefined = Reflect.getMetadata(HTTP_CODE_TOKEN, target.constructor, propertyKey);
const metaParameters: Array<MetaParameter> = (
Reflect.getMetadata(PARAMETER_TOKEN, target.constructor, propertyKey) ?? []
Expand Down
8 changes: 8 additions & 0 deletions lib/internals/loadPackage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { loadPackage } from './loadPackage';

describe('loadPackage', () => {
it('Should load a package correctly.', () => expect(loadPackage('class-validator')).not.toEqual(false));

it('Should return false for a non existing package.', () =>
expect(loadPackage('a-package-that-does-not-exist')).toEqual(false));
});
7 changes: 2 additions & 5 deletions lib/pipes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export * from './parseBoolean.pipe';
export * from './parseNumber.pipe';
export * from './validation.pipe';
export * from './validateEnum.pipe';
export * from './parseDate.pipe';
export * from './validators';
export * from './parsers';
3 changes: 3 additions & 0 deletions lib/pipes/parsers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './parseBoolean.pipe';
export * from './parseNumber.pipe';
export * from './parseDate.pipe';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseBooleanPipe } from '../../lib/pipes';
import { ParseBooleanPipe } from './parseBoolean.pipe';

describe('ParseBooleanPipe', () => {
it('Should parse the given string as boolean (true)', () => expect(ParseBooleanPipe()('true')).toStrictEqual(true));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

export function ParseBooleanPipe(options?: PipeOptions): ParameterPipe<boolean> {
return (value: any, metadata?: PipeMetadata) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseDatePipe } from '../../lib/pipes';
import { ParseDatePipe } from './parseDate.pipe';

describe('ParseDatePipe', () => {
it('Should parse the given date', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { PipeMetadata, PipeOptions } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { PipeMetadata, PipeOptions } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

// The following variables and functions are taken from the validator.js (https://github.com/validatorjs/validator.js/blob/master/src/lib/isISO8601.js)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ParseNumberPipe } from '../../lib/pipes';
import { ParseNumberPipe } from './parseNumber.pipe';

describe('ParseNumberPipe', () => {
it('Should parse the given string as number', () => expect(ParseNumberPipe()('10')).toStrictEqual(10));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

export function ParseNumberPipe(options?: PipeOptions): ParameterPipe<number> {
return (value: any, metadata?: PipeMetadata) => {
Expand Down
2 changes: 2 additions & 0 deletions lib/pipes/validators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './validation.pipe';
export * from './validateEnum.pipe';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidateEnumPipe } from '../../lib/pipes';
import { ValidateEnumPipe } from './validateEnum.pipe';

enum UserStatus {
ACTIVE = 'active',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BadRequestException } from '../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from './ParameterPipe';
import { validatePipeOptions } from './validatePipeOptions';
import { BadRequestException } from '../../exceptions';
import type { ParameterPipe, PipeOptions, PipeMetadata } from '../ParameterPipe';
import { validatePipeOptions } from '../validatePipeOptions';

interface ValidateEnumPipeOptions<T extends Record<string, unknown>> extends PipeOptions {
type: T;
Expand Down
9 changes: 9 additions & 0 deletions lib/pipes/validators/validation.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ValidationPipe } from './validation.pipe';

describe('ValidationPipe', () => {
it('Should return the value as is when there is no meta type defined.', () =>
expect(ValidationPipe()({ firstName: 'Uncle', lastName: 'Bob' })).toMatchObject({
firstName: 'Uncle',
lastName: 'Bob'
}));
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ClassTransformOptions } from 'class-transformer';
import type { ValidatorOptions } from 'class-validator';
import { validateObject } from '../internals/classValidator';
import type { ParameterPipe, PipeMetadata } from './ParameterPipe';
import { validateObject } from '../../internals/classValidator';
import type { ParameterPipe, PipeMetadata } from '../ParameterPipe';

export interface ValidationPipeOptions extends ValidatorOptions {
transformOptions?: ClassTransformOptions;
Expand Down
Loading