0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

test(core): add middleware tests [1 of 2] (#244)

* test(core): add middleware tests

add middleware tests

* fix(ut): fix typo

fix typo
This commit is contained in:
simeng-li 2022-02-17 14:21:29 +08:00 committed by GitHub
parent c1c356c30a
commit c8d45a13f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 322 additions and 14 deletions

View file

@ -2,7 +2,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['jest-matcher-specific-error'],
setupFilesAfterEnv: ['./jest.setup.js', 'jest-matcher-specific-error'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',

View file

@ -0,0 +1,12 @@
/**
* Setup environment variables for unit test
*/
const OIDC_PROVIDER_PRIVATE_KEY_BASE64 =
'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDV2dJQkFBS0JnR3pLendQcVp6Q3dncjR5a0U1NTN2aWw3QTZYM2l1VnJ3TVJtbVJDTVNBL3lkUm04bXA1CjlHZUYyMlRCSVBtUEVNM29Lbnk4KytFL2FDRnByWXVDa0loREhodVR5N1diT25nd3kyb3JpYnNEQm1OS3FybTkKM0xkYWYrZm1aU2tsL0FMUjZNeUhNV2dTUkQrbFhxVnplNFdSRGIzVTlrTyt3RmVXUlNZNmlRL2pBZ01CQUFFQwpnWUJOZkczUjVpUTFJNk1iZ0x3VGlPM3N2NURRSEE3YmtETWt4bWJtdmRacmw4TlRDemZoNnBiUEhTSFVNMUlmCkxXelVtMldYanFzQUZiOCsvUnZrWDh3OHI3SENNUUdLVGs0ay9adkZ5YUhkM2tIUXhjSkJPakNOUUtjS2NZalUKRGdnTUVJeW5PblNZNjJpWEV6RExKVTJEMVUrY3JEbTZXUTVHaG1NS1p2Vnl3UUpCQU1lcFBFV2gwakNDOEdmQwpQQU1yT1JvOHJYeHYwVEdXNlJWYmxad0ppdjhNeGZacnpZT1cwZUFPek9IK0ZRWE90SjNTdUZONzdEcVQ5TDI3CmN2M3QySkVDUVFDTGZZeVl2ZUg0UnY2bnVET0RnckkzRUJHMFNJbURHcC94UUV2NEk5Z0hrRFF0aFF4bW5xNTEKZ1QxajhFN1lmRHEwMTkvN2htL3dmMXNzMERQNkpic3pBa0JqOEUzKy9MVGRHMjJDUWpNUDB2N09KemtmWkVqdAo3WC9WOVBXNkdQeStGWUt4aWR4ZzFZbFFBWmlFTms0SGppUFNLN3VmN2hPY2JwcStyYWt0ZVhSQkFrQmhaaFFECkh5c20wbVBFTnNGNWhZdnRHTUpUOFFaYnpmNTZWUnYyc3dpSUYyL25qT3hneDFJbjZFczJlamlEdnhLNjdiV1AKQ29zbEViaFhMVFh0NStTekFrQjJQOUYzNExubE9tVjh4Zjk1VmVlcXNPbDFmWWx2Uy9vUUx1a2ZxVkJsTmtzNgpzdmNLVDJOQjlzSHlCeE8vY3Zqa0ZpWXdHR2MzNjlmQklkcDU1S2IwCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t';
const UI_SIGN_IN_ROUTE = '/sign-in';
process.env = {
...process.env,
OIDC_PROVIDER_PRIVATE_KEY_BASE64,
UI_SIGN_IN_ROUTE,
};

View file

@ -1,15 +1,5 @@
import Koa from 'koa';
/**
* Need to mock env variables ahead
*/
// eslint-disable-next-line import/order
import { envVariablesSetUp } from '@/utils/test-utils';
envVariablesSetUp();
/* eslint-disable import/first */
import * as koaErrorHandler from '@/middleware/koa-error-handler';
import * as koaI18next from '@/middleware/koa-i18next';
import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
@ -21,7 +11,6 @@ import * as initRouter from '@/routes/init';
import initI18n from '../i18n/init';
import initApp from './init';
/* eslint-enable import/first */
describe('App Init', () => {
const listenMock = jest.spyOn(Koa.prototype, 'listen').mockImplementation(jest.fn());

View file

@ -0,0 +1,90 @@
import { jwtVerify } from 'jose/jwt/verify';
import { Context } from 'koa';
import { IRouterParamContext } from 'koa-router';
import RequestError from '@/errors/RequestError';
import { createContextWithRouteParamters } from '@/utils/test-utils';
import koaAuth, { WithAuthContext } from './koa-auth';
jest.mock('jose/jwt/verify', () => ({
jwtVerify: jest.fn(() => ({ payload: { sub: 'fooUser' } })),
}));
describe('koaAuth middleware', () => {
const baseCtx = createContextWithRouteParamters();
const ctx: WithAuthContext<Context & IRouterParamContext> = {
...baseCtx,
auth: '',
};
const unauthorizedError = new RequestError({ code: 'auth.unauthorized', status: 401 });
const next = jest.fn();
beforeEach(() => {
ctx.auth = '';
ctx.request = baseCtx.request;
jest.resetModules();
});
it('should read DEVELOPMENT_USER_ID from env variable first if not production', async () => {
// Mock the @/env/consts
jest.mock('@/env/consts', () => ({
...jest.requireActual('@/env/consts'),
developmentUserId: 'foo',
}));
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable unicorn/prefer-module */
const koaAuthModule = require('./koa-auth') as { default: typeof koaAuth };
/* eslint-enable @typescript-eslint/no-require-imports */
/* eslint-enable @typescript-eslint/no-var-requires */
/* eslint-enable unicorn/prefer-module */
await koaAuthModule.default()(ctx, next);
expect(ctx.auth).toEqual('foo');
});
it('should set user auth with given sub returned from accessToken', async () => {
ctx.request = {
...ctx.request,
headers: {
authorization: 'Bearer access_token',
},
};
await koaAuth()(ctx, next);
expect(ctx.auth).toEqual('fooUser');
});
it('expect to throw if authorization header is missing', async () => {
await expect(koaAuth()(ctx, next)).rejects.toMatchError(unauthorizedError);
});
it('expect to throw if authorization header token type not recognized ', async () => {
ctx.request = {
...ctx.request,
headers: {
authorization: 'dummy access_token',
},
};
await expect(koaAuth()(ctx, next)).rejects.toMatchError(unauthorizedError);
});
it('expect to throw if jwt sub is missing', async () => {
const mockJwtVerify = jwtVerify as jest.Mock;
mockJwtVerify.mockImplementationOnce(() => ({ payload: {} }));
ctx.request = {
...ctx.request,
headers: {
authorization: 'Bearer access_token',
},
};
await expect(koaAuth()(ctx, next)).rejects.toMatchError(unauthorizedError);
});
});

View file

@ -0,0 +1,42 @@
import { createMockContext } from '@shopify/jest-koa-mocks';
import RequestError from '@/errors/RequestError';
import koaErrorHandler from './koa-error-handler';
describe('koaErrorHandler middleware', () => {
const mockBody = { data: 'foo' };
const ctx = createMockContext({
customProperties: {
body: mockBody,
},
});
const next = jest.fn().mockReturnValue(Promise.resolve());
beforeEach(() => {
jest.clearAllMocks();
ctx.body = mockBody;
ctx.status = 200;
});
it('expect to return error response if error type is RequestError', async () => {
const error = new RequestError('auth.unauthorized');
next.mockRejectedValueOnce(error);
await koaErrorHandler()(ctx, next);
expect(ctx.status).toEqual(error.status);
expect(ctx.body).toEqual(error.body);
});
it('expect to return orginal body if not error found', async () => {
await koaErrorHandler()(ctx, next);
expect(ctx.status).toEqual(200);
expect(ctx.body).toEqual(mockBody);
});
it('expect to throw if error type is not RequestError', async () => {
next.mockRejectedValueOnce(new Error('err'));
await expect(koaErrorHandler()(ctx, next)).rejects.toThrow();
});
});

View file

@ -3,10 +3,10 @@ import { Middleware } from 'koa';
import RequestError from '@/errors/RequestError';
export default function koaErrorHandler<StateT, ContextT>(): Middleware<
export default function koaErrorHandler<StateT, ContextT, BodyT>(): Middleware<
StateT,
ContextT,
RequestErrorBody
BodyT | RequestErrorBody
> {
return async (ctx, next) => {
try {

View file

@ -0,0 +1,123 @@
import { z } from 'zod';
import { emptyMiddleware, createContextWithRouteParamters } from '@/utils/test-utils';
import koaGuard, { isGuardMiddleware } from './koa-guard';
jest.mock('koa-body', () => emptyMiddleware);
describe('koaGuardMiddleware', () => {
describe('isGuardMiddleware', () => {
it('isGuardMiddleware return false if name not match', () => {
const fooMiddleware = jest.fn();
expect(isGuardMiddleware(fooMiddleware)).toEqual(false);
});
it('isGuardMiddleware return true if name is guardMiddleware & has config property', () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const guardMiddleware = () => ({});
// eslint-disable-next-line @silverhand/fp/no-mutation
guardMiddleware.config = {};
expect(isGuardMiddleware(guardMiddleware)).toBe(true);
});
it('isGuardMiddleware return false if name is name is guardMiddleware but has no config property', () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const guardMiddleware = () => ({});
expect(isGuardMiddleware(guardMiddleware)).toBe(false);
});
});
describe('guardMiddleware', () => {
const baseCtx = createContextWithRouteParamters();
const next = jest.fn();
const FooGuard = z.object({
foo: z.string(),
});
// Use to bypass the context type assert
const defaultGuard = { body: undefined, query: undefined, params: undefined };
it('invalid body type should throw', async () => {
const ctx = {
...baseCtx,
request: {
...baseCtx.request,
body: {},
},
params: {},
guard: {
...defaultGuard,
body: { foo: '1' },
},
};
await expect(koaGuard({ body: FooGuard })(ctx, next)).rejects.toThrow();
});
it('invalid query type should throw', async () => {
const ctx = {
...baseCtx,
request: {
...baseCtx.request,
query: {},
},
params: {},
guard: {
...defaultGuard,
query: { foo: '1' },
},
};
await expect(koaGuard({ query: FooGuard })(ctx, next)).rejects.toThrow();
});
it('invalid params type should throw', async () => {
const ctx = {
...baseCtx,
params: {},
guard: {
...defaultGuard,
params: { foo: '1' },
},
};
await expect(koaGuard({ params: FooGuard })(ctx, next)).rejects.toThrow();
});
it('valid body, query, params should pass', async () => {
const ctx = {
...baseCtx,
request: {
...baseCtx.request,
body: {
foo: '3',
},
query: {
foo: '2',
},
},
params: {
foo: '1',
},
guard: {
...defaultGuard,
params: { foo: '1' },
body: { foo: '1' },
query: { foo: '1' },
},
};
await koaGuard({ params: FooGuard, query: FooGuard, body: FooGuard })(ctx, next);
expect(ctx.guard.body).toHaveProperty('foo', '3');
expect(ctx.guard.query).toHaveProperty('foo', '2');
expect(ctx.guard.params).toHaveProperty('foo', '1');
});
});
});

View file

@ -0,0 +1,27 @@
import i18next from 'i18next';
import initI18n from '@/i18n/init';
import { createContextWithRouteParamters } from '@/utils/test-utils';
import koaI18next from './koa-i18next';
// Can not access outter scope function in jest mock
// eslint-disable-next-line unicorn/consistent-function-scoping
jest.mock('@/i18n/detect-language', () => () => ['zh-cn']);
const changLanguageSpy = jest.spyOn(i18next, 'changeLanguage');
describe('koaI18next', () => {
const next = jest.fn();
it('deteact language', async () => {
const ctx = {
...createContextWithRouteParamters(),
query: {},
locale: 'en',
};
await initI18n();
await koaI18next()(ctx, next);
expect(ctx.locale).toEqual('zh-CN');
expect(changLanguageSpy).toBeCalled();
});
});

View file

@ -1,3 +1,6 @@
import { createMockContext, Options } from '@shopify/jest-koa-mocks';
import { MiddlewareType, Context } from 'koa';
import Router, { IRouterParamContext } from 'koa-router';
import { createMockPool, createMockQueryResult, QueryResultRowType } from 'slonik';
import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d';
@ -33,3 +36,25 @@ export const envVariablesSetUp = () => {
UI_SIGN_IN_ROUTE,
};
};
export const emptyMiddleware =
<StateT, ContextT>(): MiddlewareType<StateT, ContextT> =>
// Intend to mock the async middleware
// eslint-disable-next-line unicorn/consistent-function-scoping
async (ctx, next) => {
return next();
};
export const createContextWithRouteParamters = (
mockContestOptions?: Options<Record<string, unknown>>
): Context & IRouterParamContext => {
const ctx = createMockContext(mockContestOptions);
return {
...ctx,
params: {},
router: new Router(),
_matchedRoute: undefined,
_matchedRouteName: undefined,
};
};