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] (#247)
* 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:
parent
52b1d92aa0
commit
9c3f017704
10 changed files with 458 additions and 8 deletions
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
116
packages/core/src/middleware/koa-oidc-error-handler.test.ts
Normal file
116
packages/core/src/middleware/koa-oidc-error-handler.test.ts
Normal 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,
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -68,7 +68,7 @@ export default function koaOIDCErrorHandler<StateT, ContextT>(): Middleware<Stat
|
|||
code: 'oidc.insufficient_scope',
|
||||
status,
|
||||
expose,
|
||||
scopes: error_detail,
|
||||
...interpolation,
|
||||
},
|
||||
data
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
88
packages/core/src/middleware/koa-ui-proxy.test.ts
Normal file
88
packages/core/src/middleware/koa-ui-proxy.test.ts
Normal 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();
|
||||
});
|
||||
});
|
55
packages/core/src/middleware/koa-user-info.test.ts
Normal file
55
packages/core/src/middleware/koa-user-info.test.ts
Normal 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 })
|
||||
);
|
||||
});
|
||||
});
|
127
packages/core/src/middleware/koa-user-log.test.ts
Normal file
127
packages/core/src/middleware/koa-user-log.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue