mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -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 = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
setupFilesAfterEnv: ['jest-matcher-specific-error'],
|
setupFilesAfterEnv: ['./jest.setup.js', 'jest-matcher-specific-error'],
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
tsconfig: 'tsconfig.test.json',
|
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';
|
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 koaErrorHandler from '@/middleware/koa-error-handler';
|
||||||
import * as koaI18next from '@/middleware/koa-i18next';
|
import * as koaI18next from '@/middleware/koa-i18next';
|
||||||
import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
|
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 initI18n from '../i18n/init';
|
||||||
import initApp from './init';
|
import initApp from './init';
|
||||||
/* eslint-enable import/first */
|
|
||||||
|
|
||||||
describe('App Init', () => {
|
describe('App Init', () => {
|
||||||
const listenMock = jest.spyOn(Koa.prototype, 'listen').mockImplementation(jest.fn());
|
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';
|
import RequestError from '@/errors/RequestError';
|
||||||
|
|
||||||
export default function koaErrorHandler<StateT, ContextT>(): Middleware<
|
export default function koaErrorHandler<StateT, ContextT, BodyT>(): Middleware<
|
||||||
StateT,
|
StateT,
|
||||||
ContextT,
|
ContextT,
|
||||||
RequestErrorBody
|
BodyT | RequestErrorBody
|
||||||
> {
|
> {
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
try {
|
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 { createMockPool, createMockQueryResult, QueryResultRowType } from 'slonik';
|
||||||
import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d';
|
import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d';
|
||||||
|
|
||||||
|
@ -33,3 +36,25 @@ export const envVariablesSetUp = () => {
|
||||||
UI_SIGN_IN_ROUTE,
|
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