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:
parent
c1c356c30a
commit
c8d45a13f0
9 changed files with 322 additions and 14 deletions
|
@ -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',
|
||||
|
|
12
packages/core/jest.setup.js
Normal file
12
packages/core/jest.setup.js
Normal 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,
|
||||
};
|
|
@ -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());
|
||||
|
|
90
packages/core/src/middleware/koa-auth.test.ts
Normal file
90
packages/core/src/middleware/koa-auth.test.ts
Normal 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);
|
||||
});
|
||||
});
|
42
packages/core/src/middleware/koa-error-handler.test.ts
Normal file
42
packages/core/src/middleware/koa-error-handler.test.ts
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
|
|
123
packages/core/src/middleware/koa-guard.test.ts
Normal file
123
packages/core/src/middleware/koa-guard.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
27
packages/core/src/middleware/koa-i18next.test.ts
Normal file
27
packages/core/src/middleware/koa-i18next.test.ts
Normal 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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue