0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

test(core): add more middleware tests [2 of 2] ()

* test(core): add more middleware tests

add more middleware tests

* fix(ut): fix koaUserInfo ut

fix koaUserInfo ut

* fix(ut): fix ut

fix ut

* fix(ut): revert chagnes

revert changes

* fix(ut): fix ut fail after schema update bug

fix ut fail after schema update bug
This commit is contained in:
simeng-li 2022-02-18 16:25:32 +08:00 committed by GitHub
parent 52b1d92aa0
commit 9c3f017704
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 458 additions and 8 deletions

View file

@ -3,7 +3,7 @@ import { Context } from 'koa';
import { IRouterParamContext } from 'koa-router';
import RequestError from '@/errors/RequestError';
import { createContextWithRouteParamters } from '@/utils/test-utils';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaAuth, { WithAuthContext } from './koa-auth';
@ -12,7 +12,7 @@ jest.mock('jose/jwt/verify', () => ({
}));
describe('koaAuth middleware', () => {
const baseCtx = createContextWithRouteParamters();
const baseCtx = createContextWithRouteParameters();
const ctx: WithAuthContext<Context & IRouterParamContext> = {
...baseCtx,

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { emptyMiddleware, createContextWithRouteParamters } from '@/utils/test-utils';
import { emptyMiddleware, createContextWithRouteParameters } from '@/utils/test-utils';
import koaGuard, { isGuardMiddleware } from './koa-guard';
@ -32,7 +32,7 @@ describe('koaGuardMiddleware', () => {
});
describe('guardMiddleware', () => {
const baseCtx = createContextWithRouteParamters();
const baseCtx = createContextWithRouteParameters();
const next = jest.fn();

View file

@ -1,7 +1,7 @@
import i18next from 'i18next';
import initI18n from '@/i18n/init';
import { createContextWithRouteParamters } from '@/utils/test-utils';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaI18next from './koa-i18next';
@ -15,7 +15,7 @@ describe('koaI18next', () => {
it('deteact language', async () => {
const ctx = {
...createContextWithRouteParamters(),
...createContextWithRouteParameters(),
query: {},
locale: 'en',
};

View file

@ -0,0 +1,116 @@
import { errors } from 'oidc-provider';
import RequestError from '@/errors/RequestError';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaOIDCErrorHandler from './koa-oidc-error-handler';
describe('koaOIDCErrorHandler middleware', () => {
const next = jest.fn();
const ctx = createContextWithRouteParameters();
it('should throw no errors if no errors are catched', async () => {
await expect(koaOIDCErrorHandler()(ctx, next)).resolves.not.toThrow();
});
it('should throw original error if error type is no OIDCProviderError', async () => {
const error = new Error('err');
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(error);
});
it('Invalid Scope', async () => {
const error_description = 'Mock scope is invalid';
const mockScope = 'read:foo';
const error = new errors.InvalidScope(error_description, mockScope);
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'oidc.invalid_scope',
status: error.status,
expose: true,
scope: mockScope,
},
{ error_description }
)
);
});
it('Session Not Found', async () => {
const error_description = 'session not found';
const error = new errors.SessionNotFound('session not found');
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'session.not_found',
status: error.status,
expose: true,
},
{
error_description,
}
)
);
});
it('Insufficient Scope', async () => {
const error_description = 'Insufficient scope for access_token';
const scope = 'read:foo';
const error = new errors.InsufficientScope(error_description, scope);
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'oidc.insufficient_scope',
status: error.status,
expose: true,
scopes: scope,
},
{
error_description,
}
)
);
});
it('Unhandled OIDCProvider Error', async () => {
const error = new errors.AuthorizationPending();
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'oidc.provider_error',
status: error.status,
expose: true,
message: error.message,
},
{
error_description: error.error_description,
error_detail: error.error_detail,
}
)
);
});
});

View file

@ -68,7 +68,7 @@ export default function koaOIDCErrorHandler<StateT, ContextT>(): Middleware<Stat
code: 'oidc.insufficient_scope',
status,
expose,
scopes: error_detail,
...interpolation,
},
data
);

View file

@ -0,0 +1,64 @@
import { NotFoundError, SlonikError } from 'slonik';
import RequestError from '@/errors/RequestError';
import { DeletionError } from '@/errors/SlonikError';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaSlonikErrorHandler from './koa-slonik-error-handler';
describe('koaSlonikErrorHandler middleware', () => {
const next = jest.fn();
const ctx = createContextWithRouteParameters();
it('should throw no errors if no errors are catched', async () => {
await expect(koaSlonikErrorHandler()(ctx, next)).resolves.not.toThrow();
});
it('should throw original error if error type are not Slonik', async () => {
const error = new Error('foo');
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(error);
});
it('should throw original error if not intend to handled', async () => {
const error = new SlonikError();
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(error);
});
it('Deletion Error', async () => {
const error = new DeletionError();
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError({
code: 'entity.not_found',
status: 404,
})
);
});
it('NotFoundError', async () => {
const error = new NotFoundError();
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError({
code: 'entity.not_found',
status: 404,
})
);
});
});

View file

@ -0,0 +1,88 @@
import { mountedApps } from '@/env/consts';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaUIProxy from './koa-ui-proxy';
const mockProxyMiddleware = jest.fn();
const mockStaticMiddleware = jest.fn();
jest.mock('fs/promises', () => ({
...jest.requireActual('fs/promises'),
readdir: jest.fn().mockResolvedValue(['sign-in']),
}));
jest.mock('koa-proxies', () => jest.fn(() => mockProxyMiddleware));
jest.mock('koa-static', () => jest.fn(() => mockStaticMiddleware));
describe('koaUIProxy middleware', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
const next = jest.fn();
for (const app of mountedApps) {
// eslint-disable-next-line @typescript-eslint/no-loop-func
it(`${app} path should not call uiProxy`, async () => {
const ctx = createContextWithRouteParameters({
url: `/${app}/foo`,
});
await koaUIProxy()(ctx, next);
expect(mockProxyMiddleware).not.toBeCalled();
});
}
it('dev env should call proxy middleware for ui paths', async () => {
const ctx = createContextWithRouteParameters();
await koaUIProxy()(ctx, next);
expect(mockProxyMiddleware).toBeCalled();
});
it('production env should overwrite the request path to root if no target ui file are detected', async () => {
// Mock the @/env/consts
jest.mock('@/env/consts', () => ({
...jest.requireActual('@/env/consts'),
isProduction: true,
}));
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable unicorn/prefer-module */
const koaUIProxyModule = require('./koa-ui-proxy') as { default: typeof koaUIProxy };
/* eslint-enable @typescript-eslint/no-require-imports */
/* eslint-enable @typescript-eslint/no-var-requires */
/* eslint-enable unicorn/prefer-module */
const ctx = createContextWithRouteParameters({
url: '/foo',
});
await koaUIProxyModule.default()(ctx, next);
expect(mockStaticMiddleware).toBeCalled();
expect(ctx.request.path).toEqual('/');
});
it('production env should call the static middleware if path hit the ui file directory', async () => {
// Mock the @/env/consts
jest.mock('@/env/consts', () => ({
...jest.requireActual('@/env/consts'),
isProduction: true,
}));
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable unicorn/prefer-module */
const koaUIProxyModule = require('./koa-ui-proxy') as { default: typeof koaUIProxy };
/* eslint-enable @typescript-eslint/no-require-imports */
/* eslint-enable @typescript-eslint/no-var-requires */
/* eslint-enable unicorn/prefer-module */
const ctx = createContextWithRouteParameters({
url: '/sign-in',
});
await koaUIProxyModule.default()(ctx, next);
expect(mockStaticMiddleware).toBeCalled();
});
});

View file

@ -0,0 +1,55 @@
import { User, userInfoSelectFields } from '@logto/schemas';
import pick from 'lodash.pick';
import RequestError from '@/errors/RequestError';
import * as userQueries from '@/queries/user';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaUserInfo from './koa-user-info';
const findUserByIdSpy = jest.spyOn(userQueries, 'findUserById');
const mockUser: User = {
id: 'foo',
username: 'foo',
primaryEmail: 'foo@logto.io',
primaryPhone: '111111',
roleNames: ['admin'],
passwordEncrypted: null,
passwordEncryptionMethod: null,
passwordEncryptionSalt: null,
name: null,
avatar: null,
identities: {},
customData: {},
};
describe('koaUserInfo middleware', () => {
const next = jest.fn();
it('should set userInfo to the context', async () => {
findUserByIdSpy.mockImplementationOnce(async () => Promise.resolve(mockUser));
const ctx = {
...createContextWithRouteParameters(),
auth: 'foo',
userInfo: { id: '' }, // Bypass the middleware Context type
};
await koaUserInfo()(ctx, next);
expect(ctx.userInfo).toEqual(pick(mockUser, ...userInfoSelectFields));
});
it('should throw if is not authenticated', async () => {
const ctx = {
...createContextWithRouteParameters(),
auth: 'foo',
userInfo: { id: '' }, // Bypass the middleware Context type
};
await expect(koaUserInfo()(ctx, next)).rejects.toMatchError(
new RequestError({ code: 'auth.unauthorized', status: 401 })
);
});
});

View file

@ -0,0 +1,127 @@
import { UserLogType, UserLogResult } from '@logto/schemas';
import { insertUserLog } from '@/queries/user-log';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaUserLog, { WithUserLogContext, LogContext } from './koa-user-log';
const nanoIdMock = 'mockId';
jest.mock('@/queries/user-log', () => ({
insertUserLog: jest.fn(async () => Promise.resolve()),
}));
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => nanoIdMock),
}));
describe('koaUserLog middleware', () => {
const insertUserLogMock = insertUserLog as jest.Mock;
const next = jest.fn();
const userLogMock: Partial<LogContext> = {
userId: 'foo',
type: UserLogType.SignInEmail,
email: 'foo@logto.io',
payload: { applicationId: 'foo' },
};
beforeEach(() => {
jest.clearAllMocks();
});
it('insert userLog with success response', async () => {
const ctx: WithUserLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
userLog: {
payload: {},
createdAt: 0,
}, // Bypass middleware context type assert
};
next.mockImplementationOnce(async () => {
ctx.userLog = {
...ctx.userLog,
...userLogMock,
};
});
await koaUserLog()(ctx, next);
expect(ctx.userLog).toHaveProperty('userId', userLogMock.userId);
expect(ctx.userLog).toHaveProperty('type', userLogMock.type);
expect(ctx.userLog).toHaveProperty('email', userLogMock.email);
expect(ctx.userLog).toHaveProperty('payload', userLogMock.payload);
expect(ctx.userLog.createdAt).not.toBeFalsy();
expect(insertUserLogMock).toBeCalledWith({
id: nanoIdMock,
userId: ctx.userLog.userId,
type: ctx.userLog.type,
result: UserLogResult.Success,
payload: ctx.userLog.payload,
});
});
it('should not block request if insertLog throws error', async () => {
const ctx: WithUserLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
userLog: {
payload: {},
createdAt: 0,
}, // Bypass middleware context type assert
};
insertUserLogMock.mockRejectedValue(new Error(' '));
next.mockImplementationOnce(async () => {
ctx.userLog = {
...ctx.userLog,
...userLogMock,
};
});
await koaUserLog()(ctx, next);
expect(ctx.userLog).toHaveProperty('userId', userLogMock.userId);
expect(ctx.userLog).toHaveProperty('type', userLogMock.type);
expect(ctx.userLog).toHaveProperty('email', userLogMock.email);
expect(ctx.userLog).toHaveProperty('payload', userLogMock.payload);
expect(ctx.userLog.createdAt).not.toBeFalsy();
expect(insertUserLogMock).toBeCalledWith({
id: nanoIdMock,
userId: ctx.userLog.userId,
type: ctx.userLog.type,
result: UserLogResult.Success,
payload: ctx.userLog.payload,
});
});
it('should insert userLog with failed result if next throws error', async () => {
const ctx: WithUserLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
userLog: {
payload: {},
createdAt: 0,
}, // Bypass middleware context type assert
};
const error = new Error('next error');
next.mockImplementationOnce(async () => {
ctx.userLog = {
...ctx.userLog,
...userLogMock,
};
throw error;
});
await expect(koaUserLog()(ctx, next)).rejects.toMatchError(error);
expect(ctx.userLog.createdAt).not.toBeFalsy();
expect(insertUserLogMock).toBeCalledWith({
id: nanoIdMock,
userId: ctx.userLog.userId,
type: ctx.userLog.type,
result: UserLogResult.Failed,
payload: ctx.userLog.payload,
});
});
});

View file

@ -45,7 +45,7 @@ export const emptyMiddleware =
return next();
};
export const createContextWithRouteParamters = (
export const createContextWithRouteParameters = (
mockContestOptions?: Options<Record<string, unknown>>
): Context & IRouterParamContext => {
const ctx = createMockContext(mockContestOptions);