Add first test for JSONRPC service
This commit is contained in:
parent
0e69cf239a
commit
3739f27ebe
@ -1,6 +1,7 @@
|
|||||||
import {ICredentials} from './ICredentials'
|
import {ICredentials} from './ICredentials'
|
||||||
import {IUser} from './IUser'
|
import {IUser} from './IUser'
|
||||||
import * as e from './entities'
|
import * as e from './entities'
|
||||||
|
import {keys} from 'ts-transformer-keys'
|
||||||
|
|
||||||
export interface IChangePasswordParams {
|
export interface IChangePasswordParams {
|
||||||
oldPassword: string
|
oldPassword: string
|
||||||
@ -13,8 +14,8 @@ export interface ICreateUserParams extends ICredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserService {
|
export interface IUserService {
|
||||||
changePassword(params: IChangePasswordParams): Promise<void>
|
getProfile(): Promise<IUser>
|
||||||
// validateCredentials(credentials: ICredentials): Promise<e.User | undefined>
|
|
||||||
findOne(id: number): Promise<IUser | undefined>
|
|
||||||
findUserByEmail(email: string): Promise<IUser | undefined>
|
findUserByEmail(email: string): Promise<IUser | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UserServiceMethods = keys<IUserService>()
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const constantId = (val: string) => () => val
|
|||||||
export function createRemoteClient<T>(
|
export function createRemoteClient<T>(
|
||||||
url: string,
|
url: string,
|
||||||
methods: Array<FunctionPropertyNames<T>>,
|
methods: Array<FunctionPropertyNames<T>>,
|
||||||
|
headers: Record<string, string> = {},
|
||||||
getNextRequestId: TRequestIdGenerator<string | number> = constantId('c'),
|
getNextRequestId: TRequestIdGenerator<string | number> = constantId('c'),
|
||||||
idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX,
|
idempotentMethodRegex = IDEMPOTENT_METHOD_REGEX,
|
||||||
) {
|
) {
|
||||||
@ -26,6 +27,7 @@ export function createRemoteClient<T>(
|
|||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: reqMethod,
|
method: reqMethod,
|
||||||
url,
|
url,
|
||||||
|
headers,
|
||||||
[payloadKey]: {
|
[payloadKey]: {
|
||||||
id,
|
id,
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import * as routes from '../routes'
|
|||||||
import { TransactionalRouter } from '../router'
|
import { TransactionalRouter } from '../router'
|
||||||
import { IRoutes, IContext } from '@rondo.dev/common'
|
import { IRoutes, IContext } from '@rondo.dev/common'
|
||||||
import { Express } from 'express-serve-static-core'
|
import { Express } from 'express-serve-static-core'
|
||||||
|
import { jsonrpc, bulkjsonrpc } from '@rondo.dev/jsonrpc'
|
||||||
|
import * as rpc from '../rpc'
|
||||||
|
|
||||||
export type ServerConfigurator<
|
export type ServerConfigurator<
|
||||||
T extends IServerConfig = IServerConfig
|
T extends IServerConfig = IServerConfig
|
||||||
@ -31,6 +33,21 @@ export const configureServer: ServerConfigurator = (config, database) => {
|
|||||||
userPermissions: new User.UserPermissions(database),
|
userPermissions: new User.UserPermissions(database),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rpcServices = {
|
||||||
|
userService: new rpc.UserService(database),
|
||||||
|
teamService: new rpc.TeamService(database, services.userPermissions),
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContext = (req: Express.Request): IContext => ({user: req.user})
|
||||||
|
|
||||||
|
const rpcMiddleware = jsonrpc(
|
||||||
|
req => getContext(req),
|
||||||
|
logger,
|
||||||
|
// (details, invoke) => database
|
||||||
|
// .transactionManager
|
||||||
|
// .doInNewTransaction(() => invoke()),
|
||||||
|
)
|
||||||
|
|
||||||
const authenticator = new Middleware.Authenticator(services.authService)
|
const authenticator = new Middleware.Authenticator(services.authService)
|
||||||
const transactionManager = database.transactionManager
|
const transactionManager = database.transactionManager
|
||||||
|
|
||||||
@ -90,6 +107,10 @@ export const configureServer: ServerConfigurator = (config, database) => {
|
|||||||
],
|
],
|
||||||
error: new Middleware.ErrorApiHandler(logger).handle,
|
error: new Middleware.ErrorApiHandler(logger).handle,
|
||||||
},
|
},
|
||||||
|
rpc: {
|
||||||
|
path: '/rpc',
|
||||||
|
handle: [bulkjsonrpc(rpcMiddleware, rpcServices)],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
packages/server/src/rpc/UserService.test.ts
Normal file
48
packages/server/src/rpc/UserService.test.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {test} from '../test'
|
||||||
|
import {user} from '@rondo.dev/common'
|
||||||
|
|
||||||
|
describe('user', () => {
|
||||||
|
|
||||||
|
test.withDatabase()
|
||||||
|
|
||||||
|
let cookie!: string
|
||||||
|
let token!: string
|
||||||
|
let headers: Record<string, string> = {}
|
||||||
|
beforeEach(async () => {
|
||||||
|
await test.registerAccount()
|
||||||
|
const session = await test.login()
|
||||||
|
cookie = session.cookie
|
||||||
|
token = session.token
|
||||||
|
headers = {cookie, 'x-csrf-token': token}
|
||||||
|
})
|
||||||
|
|
||||||
|
const createService = () => {
|
||||||
|
return test.rpc<user.IUserService>(
|
||||||
|
'/rpc/userService',
|
||||||
|
user.UserServiceMethods,
|
||||||
|
headers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('findOne', () => {
|
||||||
|
it('fetches current user\'s profile', async () => {
|
||||||
|
const profile = await createService().getProfile()
|
||||||
|
expect(profile.id).toEqual(jasmine.any(Number))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('findUserByEmail', () => {
|
||||||
|
it('returns undefined when user no found', async () => {
|
||||||
|
const profile = await createService().findUserByEmail(
|
||||||
|
'totallynonexisting@email.com')
|
||||||
|
expect(profile).toBe(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns user profile when found', async () => {
|
||||||
|
const profile = await createService().findUserByEmail(
|
||||||
|
test.username)
|
||||||
|
expect(profile).not.toBe(undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
@ -13,33 +13,13 @@ const MIN_PASSWORD_LENGTH = 10
|
|||||||
export class UserService implements RPC<u.IUserService> {
|
export class UserService implements RPC<u.IUserService> {
|
||||||
constructor(protected readonly db: IDatabase) {}
|
constructor(protected readonly db: IDatabase) {}
|
||||||
|
|
||||||
async changePassword(context: IContext, params: u.IChangePasswordParams) {
|
async getProfile(context: IContext) {
|
||||||
const userId = context.user!.id
|
const userId = context.user!.id
|
||||||
const {oldPassword, newPassword} = params
|
|
||||||
const userRepository = this.db.getRepository(User)
|
|
||||||
const user = await userRepository
|
|
||||||
.createQueryBuilder('user')
|
|
||||||
.select('user')
|
|
||||||
.addSelect('user.password')
|
|
||||||
.whereInIds([ userId ])
|
|
||||||
.getOne()
|
|
||||||
const isValid = await compare(oldPassword, user ? user.password! : '')
|
|
||||||
if (!(user && isValid)) {
|
|
||||||
throw createError(400, 'Passwords do not match')
|
|
||||||
}
|
|
||||||
const password = await this._hash(newPassword)
|
|
||||||
await userRepository
|
|
||||||
.update(userId, { password })
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(context: IContext, id: number) {
|
// current user should always exist in the database
|
||||||
const user = await this.db.getRepository(User).findOne(id, {
|
const user = (await this.db.getRepository(User).findOne(userId, {
|
||||||
relations: ['emails'],
|
relations: ['emails'],
|
||||||
})
|
}))!
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|||||||
@ -10,6 +10,9 @@ import {RequestTester} from './RequestTester'
|
|||||||
import {Role} from '../entities/Role'
|
import {Role} from '../entities/Role'
|
||||||
import {CORRELATION_ID} from '../middleware'
|
import {CORRELATION_ID} from '../middleware'
|
||||||
import shortid from 'shortid'
|
import shortid from 'shortid'
|
||||||
|
import { AddressInfo } from 'net'
|
||||||
|
import { createRemoteClient, FunctionPropertyNames, TAsyncified } from '@rondo.dev/jsonrpc'
|
||||||
|
import {Server} from 'http'
|
||||||
|
|
||||||
export class TestUtils<T extends IRoutes> {
|
export class TestUtils<T extends IRoutes> {
|
||||||
readonly username = `test${process.env.JEST_WORKER_ID}@user.com`
|
readonly username = `test${process.env.JEST_WORKER_ID}@user.com`
|
||||||
@ -160,6 +163,39 @@ export class TestUtils<T extends IRoutes> {
|
|||||||
`${this.bootstrap.getConfig().app.baseUrl.path!}${baseUrl}`)
|
`${this.bootstrap.getConfig().app.baseUrl.path!}${baseUrl}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server, invokes a rpc method, and closes the server after
|
||||||
|
* invocation.
|
||||||
|
*/
|
||||||
|
rpc = <S>(
|
||||||
|
serviceUrl: string,
|
||||||
|
methods: Array<FunctionPropertyNames<S>>,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
) => {
|
||||||
|
const {app} = this
|
||||||
|
const url = `${this.bootstrap.getConfig().app.baseUrl.path!}${serviceUrl}`
|
||||||
|
|
||||||
|
const service = methods.reduce((obj, method) => {
|
||||||
|
obj[method] = async function makeRequest(...args: any[]) {
|
||||||
|
let server!: Server
|
||||||
|
await new Promise(resolve => {
|
||||||
|
server = app.listen(0, '127.0.0.1', resolve)
|
||||||
|
})
|
||||||
|
const addr = server.address() as AddressInfo
|
||||||
|
const fullUrl = `http://${addr.address}:${addr.port}${url}`
|
||||||
|
const remoteService = createRemoteClient<S>(fullUrl, methods, headers)
|
||||||
|
try {
|
||||||
|
return await remoteService[method](...args as any)
|
||||||
|
} finally {
|
||||||
|
await new Promise(resolve => server.close(resolve))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}, {} as any)
|
||||||
|
|
||||||
|
return service as TAsyncified<S>
|
||||||
|
}
|
||||||
|
|
||||||
private getCookies(setCookiesString: string[]): string {
|
private getCookies(setCookiesString: string[]): string {
|
||||||
return setCookiesString.map(c => c.split('; ')[0]).join('; ')
|
return setCookiesString.map(c => c.split('; ')[0]).join('; ')
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user