0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

test(core): add admin-user route UT (#252)

* test(core): add admin-user route ut

add admin-user route ut

* fix(ut): remove redundent code

remove mockKoaAuthMiddleware

* feat(core): update pnpm.lock

update pnpm.lock
This commit is contained in:
simeng-li 2022-02-21 10:45:31 +08:00 committed by GitHub
parent 47ccb4da02
commit 8fa169cba1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 283 additions and 34 deletions

View file

@ -59,6 +59,7 @@
"@types/lodash.pick": "^4.4.6",
"@types/node": "^16.3.1",
"@types/oidc-provider": "^7.8.0",
"@types/supertest": "^2.0.11",
"codecov": "^3.8.3",
"eslint": "^8.1.0",
"jest": "^27.0.6",
@ -67,6 +68,7 @@
"nock": "^13.2.2",
"openapi-types": "^9.1.0",
"prettier": "^2.3.2",
"supertest": "^6.2.2",
"ts-jest": "^27.0.5",
"tsc-watch": "^4.4.0",
"typescript": "^4.5.5"

View file

@ -1,29 +1,11 @@
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 { mockUser, mockUserResponse, 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();
@ -38,7 +20,7 @@ describe('koaUserInfo middleware', () => {
await koaUserInfo()(ctx, next);
expect(ctx.userInfo).toEqual(pick(mockUser, ...userInfoSelectFields));
expect(ctx.userInfo).toEqual(mockUserResponse);
});
it('should throw if is not authenticated', async () => {

View file

@ -0,0 +1,162 @@
import { CreateUser, User } from '@logto/schemas';
import Koa from 'koa';
import Router from 'koa-router';
import request from 'supertest';
import { hasUser, findUserById } from '@/queries/user';
import { mockUser, mockUserResponse } from '@/utils/test-utils';
import adminUserRoutes from './admin-user';
import { AuthedRouter } from './types';
jest.mock('@/queries/user', () => ({
findTotalNumberOfUsers: jest.fn(async () => ({ count: 10 })),
findAllUsers: jest.fn(async (): Promise<User[]> => [mockUser]),
findUserById: jest.fn(async (): Promise<User> => mockUser),
hasUser: jest.fn(async () => false),
updateUserById: jest.fn(
async (_, data: Partial<CreateUser>): Promise<User> => ({
...mockUser,
...data,
})
),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
),
}));
jest.mock('@/lib/user', () => ({
generateUserId: jest.fn(() => 'fooId'),
encryptUserPassword: jest.fn(() => ({
passwordEncryptionSalt: 'salt',
passwordEncrypted: 'password',
passwordEncryptionMethod: 'saltAndPepper',
})),
}));
describe('adminUserRoutes', () => {
const app = new Koa();
const router: AuthedRouter = new Router();
adminUserRoutes(router);
app.use(router.routes()).use(router.allowedMethods());
const userRequest = request(app.callback());
it('GET /users', async () => {
const response = await userRequest.get('/users');
expect(response.status).toEqual(200);
expect(response.body).toEqual([mockUserResponse]);
});
it('GET /users/:userId', async () => {
const response = await userRequest.get('/users/foo');
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockUserResponse);
});
it('POST /users', async () => {
const username = 'MJ@logto.io';
const password = 'PASSWORD';
const name = 'Micheal';
const response = await userRequest.post('/users').send({ username, password, name });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
id: 'fooId',
username,
name,
});
});
it('POST /users should throw with invalid input params', async () => {
const username = 'MJ@logto.io';
const password = 'PASSWORD';
const name = 'Micheal';
// Missing input
await expect(userRequest.post('/users').send({})).resolves.toHaveProperty('status', 400);
await expect(userRequest.post('/users').send({ username, password })).resolves.toHaveProperty(
'status',
400
);
await expect(userRequest.post('/users').send({ username, name })).resolves.toHaveProperty(
'status',
400
);
await expect(userRequest.post('/users').send({ password, name })).resolves.toHaveProperty(
'status',
400
);
// Invalid input format
await expect(
userRequest.post('/users').send({ username: 'xy', password, name })
).resolves.toHaveProperty('status', 400);
await expect(
userRequest.post('/users').send({ username, password: 'abc', name })
).resolves.toHaveProperty('status', 400);
await expect(
userRequest.post('/users').send({ username, password, name: 'xy' })
).resolves.toHaveProperty('status', 400);
});
it('POST /users should throw if username exist', async () => {
const mockHasUser = hasUser as jest.Mock;
mockHasUser.mockImplementationOnce(async () => Promise.resolve(true));
const username = 'MJ@logto.io';
const password = 'PASSWORD';
const name = 'Micheal';
await expect(
userRequest.post('/users').send({ username, password, name })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId', async () => {
const name = 'Micheal';
const avatar = 'http://www.micheal.png';
const response = await userRequest.patch('/users/foo').send({ name, avatar });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
name,
avatar,
});
});
it('PATCH /users/:userId throw with invalid input params', async () => {
const name = 'Micheal';
const avatar = 'http://www.micheal.png';
await expect(userRequest.patch('/users/foo').send({ avatar })).resolves.toHaveProperty(
'status',
200
);
await expect(
userRequest.patch('/users/foo').send({ name, avatar: 'non url' })
).resolves.toHaveProperty('status', 400);
});
it('PATCH /users/:userId throw if user not found', async () => {
const name = 'Micheal';
const avatar = 'http://www.micheal.png';
const mockFindUserById = findUserById as jest.Mock;
mockFindUserById.mockImplementationOnce(() => {
throw new Error(' ');
});
await expect(userRequest.patch('/users/foo').send({ name, avatar })).resolves.toHaveProperty(
'status',
500
);
});
});

View file

@ -39,6 +39,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
router.get(
'/users/:userId',
// TODO: No need to guard
koaGuard({
params: object({ userId: string() }),
}),
@ -66,7 +67,6 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
}),
async (ctx, next) => {
const { username, password, name } = ctx.guard.body;
assertThat(
!(await hasUser(username)),
new RequestError({

View file

@ -1,9 +1,28 @@
import { User, userInfoSelectFields } from '@logto/schemas';
import { createMockContext, Options } from '@shopify/jest-koa-mocks';
import { MiddlewareType, Context } from 'koa';
import Router, { IRouterParamContext } from 'koa-router';
import pick from 'lodash.pick';
import { createMockPool, createMockQueryResult, QueryResultRowType } from 'slonik';
import { PrimitiveValueExpressionType } from 'slonik/dist/src/types.d';
export 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: {},
};
export const mockUserResponse = pick(mockUser, ...userInfoSelectFields);
export const createTestPool = <T extends QueryResultRowType>(
expectSql?: string,
returning?: T | ((sql: string, values: readonly PrimitiveValueExpressionType[]) => T)
@ -25,18 +44,6 @@ export const createTestPool = <T extends QueryResultRowType>(
},
});
export const envVariablesSetUp = () => {
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,
};
};
export const emptyMiddleware =
<StateT, ContextT>(): MiddlewareType<StateT, ContextT> =>
// Intend to mock the async middleware

View file

@ -80,6 +80,7 @@ importers:
'@types/lodash.pick': ^4.4.6
'@types/node': ^16.3.1
'@types/oidc-provider': ^7.8.0
'@types/supertest': ^2.0.11
codecov: ^3.8.3
dayjs: ^1.10.5
decamelize: ^5.0.0
@ -111,6 +112,7 @@ importers:
slonik: ^23.8.3
slonik-interceptor-preset: ^1.2.10
snakecase-keys: ^5.1.0
supertest: ^6.2.2
ts-jest: ^27.0.5
tsc-watch: ^4.4.0
typescript: ^4.5.5
@ -156,6 +158,7 @@ importers:
'@types/lodash.pick': 4.4.6
'@types/node': 16.11.12
'@types/oidc-provider': 7.8.1
'@types/supertest': 2.0.11
codecov: 3.8.3
eslint: 8.4.1
jest: 27.4.4
@ -164,6 +167,7 @@ importers:
nock: 13.2.2
openapi-types: 9.3.1
prettier: 2.5.1
supertest: 6.2.2
ts-jest: 27.1.1_0ef321b3552d50570980838f9f6677eb
tsc-watch: 4.5.0_typescript@4.5.5
typescript: 4.5.5
@ -3074,6 +3078,10 @@ packages:
resolution: {integrity: sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==}
dev: true
/@types/cookiejar/2.1.2:
resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==}
dev: true
/@types/cookies/0.7.7:
resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==}
dependencies:
@ -3354,6 +3362,19 @@ packages:
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
dev: true
/@types/superagent/4.1.15:
resolution: {integrity: sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ==}
dependencies:
'@types/cookiejar': 2.1.2
'@types/node': 16.11.12
dev: true
/@types/supertest/2.0.11:
resolution: {integrity: sha512-uci4Esokrw9qGb9bvhhSVEjd6rkny/dk5PK/Qz4yxKiyppEI+dOPlNrZBahE3i+PoKFYyDxChVXZ/ysS/nrm1Q==}
dependencies:
'@types/superagent': 4.1.15
dev: true
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: true
@ -4416,6 +4437,10 @@ packages:
dot-prop: 5.3.0
dev: true
/component-emitter/1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
dev: true
/concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
@ -4553,6 +4578,10 @@ packages:
safe-buffer: 5.1.2
dev: true
/cookiejar/2.1.3:
resolution: {integrity: sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==}
dev: true
/cookies/0.8.0:
resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==}
engines: {node: '>= 0.8'}
@ -5787,6 +5816,10 @@ packages:
boolean: 3.1.4
dev: false
/fast-safe-stringify/2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
dev: true
/fast-url-parser/1.1.3:
resolution: {integrity: sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=}
dependencies:
@ -5907,11 +5940,29 @@ packages:
mime-types: 2.1.34
dev: true
/form-data/4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.34
dev: true
/formidable/1.2.6:
resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==}
deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau'
dev: false
/formidable/2.0.1:
resolution: {integrity: sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==}
dependencies:
dezalgo: 1.0.3
hexoid: 1.0.0
once: 1.4.0
qs: 6.9.3
dev: true
/fresh/0.5.2:
resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=}
engines: {node: '>= 0.6'}
@ -6313,6 +6364,11 @@ packages:
dependencies:
function-bind: 1.1.1
/hexoid/1.0.0:
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
engines: {node: '>=8'}
dev: true
/history/4.10.1:
resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==}
dependencies:
@ -8196,7 +8252,7 @@ packages:
dependencies:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.8
graceful-fs: 4.2.9
dev: true
/jsonparse/1.3.1:
@ -8889,6 +8945,12 @@ packages:
hasBin: true
dev: true
/mime/2.6.0:
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
engines: {node: '>=4.0.0'}
hasBin: true
dev: true
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@ -10735,6 +10797,11 @@ packages:
engines: {node: '>=0.6'}
dev: true
/qs/6.9.3:
resolution: {integrity: sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==}
engines: {node: '>=0.6'}
dev: true
/query-string/6.14.1:
resolution: {integrity: sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==}
engines: {node: '>=6'}
@ -12027,6 +12094,35 @@ packages:
postcss: 7.0.39
dev: true
/superagent/7.1.1:
resolution: {integrity: sha512-CQ2weSS6M+doIwwYFoMatklhRbx6sVNdB99OEJ5czcP3cng76Ljqus694knFWgOj3RkrtxZqIgpe6vhe0J7QWQ==}
engines: {node: '>=6.4.0 <13 || >=14'}
dependencies:
component-emitter: 1.3.0
cookiejar: 2.1.3
debug: 4.3.3
fast-safe-stringify: 2.1.1
form-data: 4.0.0
formidable: 2.0.1
methods: 1.1.2
mime: 2.6.0
qs: 6.10.2
readable-stream: 3.6.0
semver: 7.3.5
transitivePeerDependencies:
- supports-color
dev: true
/supertest/6.2.2:
resolution: {integrity: sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg==}
engines: {node: '>=6.0.0'}
dependencies:
methods: 1.1.2
superagent: 7.1.1
transitivePeerDependencies:
- supports-color
dev: true
/supports-color/5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}