account tests

Signed-off-by: Andrey Platov <andrey@hardcoreeng.com>
This commit is contained in:
Andrey Platov 2021-09-09 22:04:00 +02:00
parent 6011a75e05
commit 40950b5ca9
No known key found for this signature in database
GPG Key ID: C8787EFEB4B64AF0
2 changed files with 288 additions and 8 deletions

View File

@ -0,0 +1,157 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { MongoClient, Db } from 'mongodb'
import { methods, getAccount, getWorkspace } from '..'
import { randomBytes } from 'crypto'
const DB_NAME = 'test_accounts'
describe('server', () => {
const dbUri = process.env.MONGODB_URI ?? 'mongodb://localhost:27017'
let conn: MongoClient
let db: Db
let workspace: string = 'ws-' + randomBytes(8).toString('hex')
beforeAll(async () => {
conn = await MongoClient.connect(dbUri)
})
beforeEach(async () => {
const olddb = conn.db(DB_NAME)
await olddb.dropDatabase()
db = conn.db(DB_NAME)
await db.collection('account').createIndex({ email: 1 }, { unique: true })
await db.collection('workspace').createIndex({ workspace: 1 }, { unique: true })
})
it('should create workspace', async () => {
const request: any = {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
}
const result = await methods.createWorkspace(db, request)
expect(result.result).toBeDefined()
workspace = result.result as string
})
it('should create account', async () => {
const request: any = {
method: 'createAccount',
params: ['andrey2', '123']
}
const result = await methods.createAccount(db, request)
expect(result.result).toBeDefined()
})
it('should not create, duplicate account', async () => {
await methods.createAccount(db, {
method: 'createAccount',
params: ['andrey', '123']
})
const request: any = {
method: 'createAccount',
params: ['andrey', '123']
}
const result = await methods.createAccount(db, request)
expect(result.error).toBeDefined()
})
it('should login', async () => {
await methods.createAccount(db, {
method: 'createAccount',
params: ['andrey', '123']
})
await methods.createWorkspace(db, {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
await methods.assignWorkspace(db, {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
const request: any = {
method: 'login',
params: ['andrey', '123', workspace]
}
const result = await methods.login(db, request)
expect(result.result).toBeDefined()
})
it('should not login, wrong password', async () => {
const request: any = {
method: 'login',
params: ['andrey', '123555', workspace]
}
const result = await methods.login(db, request)
expect(result.error).toBeDefined()
})
it('should not login, unknown user', async () => {
const request: any = {
method: 'login',
params: ['andrey1', '123555', workspace]
}
const result = await methods.login(db, request)
expect(result.error).toBeDefined()
})
it('should not login, wrong workspace', async () => {
const request: any = {
method: 'login',
params: ['andrey', '123', 'non-existent-workspace']
}
const result = await methods.login(db, request)
expect(result.error).toBeDefined()
})
it('do remove workspace', async () => {
await methods.createAccount(db, {
method: 'createAccount',
params: ['andrey', '123']
})
await methods.createWorkspace(db, {
method: 'createWorkspace',
params: [workspace, 'ООО Рога и Копыта']
})
await methods.assignWorkspace(db, {
method: 'assignWorkspace',
params: ['andrey', workspace]
})
// Check we had one
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(1)
expect((await getWorkspace(db, workspace))?.accounts.length).toEqual(1)
await methods.removeWorkspace(db, {
method: 'removeWorkspace',
params: ['andrey', workspace]
})
expect((await getAccount(db, 'andrey'))?.workspaces.length).toEqual(0)
expect((await getWorkspace(db, workspace))?.accounts.length).toEqual(0)
})
afterAll(async () => {
await conn.close()
})
})

View File

@ -14,10 +14,10 @@
// limitations under the License.
//
import type { Plugin, StatusCode } from '@anticrm/platform'
import { PlatformError, Severity, Status, plugin } from '@anticrm/platform'
import type { Plugin, StatusCode, Request, Response } from '@anticrm/platform'
import { PlatformError, Severity, Status, plugin, unknownStatus } from '@anticrm/platform'
import { Binary, Db, ObjectId } from 'mongodb'
import { pbkdf2Sync } from 'crypto'
import { pbkdf2Sync, randomBytes } from 'crypto'
import { encode } from 'jwt-simple'
const WORKSPACE_COLLECTION = 'workspace'
@ -37,12 +37,18 @@ export const accountId = 'account' as Plugin
const accountPlugin = plugin(accountId, {
status: {
AccountNotFound: '' as StatusCode<{account: string}>,
WorkspaceNotFound: '' as StatusCode<{workspace: string}>,
InvalidPassword: '' as StatusCode<{account: string}>,
AccountAlreadyExists: '' as StatusCode<{account: string}>,
WorkspaceAlreadyExists: '' as StatusCode<{workspace: string}>,
Forbidden: '' as StatusCode
}
})
interface Account {
/**
* @public
*/
export interface Account {
_id: ObjectId
email: string
hash: Binary
@ -50,7 +56,10 @@ interface Account {
workspaces: ObjectId[]
}
interface Workspace {
/**
* @public
*/
export interface Workspace {
_id: ObjectId
workspace: string
organisation: string
@ -66,7 +75,10 @@ export interface LoginInfo {
endpoint: string
}
type AccountInfo = Omit<Account, 'hash' | 'salt'>
/**
* @public
*/
export type AccountInfo = Omit<Account, 'hash' | 'salt'>
function hashWithSalt (password: string, salt: Buffer): Buffer {
return pbkdf2Sync(password, salt, 1000, 32, 'sha256')
@ -76,11 +88,20 @@ function verifyPassword (password: string, hash: Buffer, salt: Buffer): boolean
return Buffer.compare(hash, hashWithSalt(password, salt)) === 0
}
async function getAccount (db: Db, email: string): Promise<Account | null> {
/**
* @public
*/
export async function getAccount (db: Db, email: string): Promise<Account | null> {
return await db.collection(ACCOUNT_COLLECTION).findOne<Account>({ email })
}
async function getWorkspace (db: Db, workspace: string): Promise<Workspace | null> {
/**
* @public
* @param db -
* @param workspace -
* @returns
*/
export async function getWorkspace (db: Db, workspace: string): Promise<Workspace | null> {
return await db.collection(WORKSPACE_COLLECTION).findOne<Workspace>({
workspace
})
@ -137,4 +158,106 @@ export async function login (db: Db, email: string, password: string, workspace:
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.Forbidden, {}))
}
/**
* @public
*/
export async function createAccount (db: Db, email: string, password: string): Promise<AccountInfo> {
const salt = randomBytes(32)
const hash = hashWithSalt(password, salt)
const account = await getAccount(db, email)
if (account !== null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountAlreadyExists, { account: email }))
}
const insert = await db.collection(ACCOUNT_COLLECTION).insertOne({
email,
hash,
salt,
workspaces: []
})
return {
_id: insert.insertedId,
email,
workspaces: []
}
}
/**
* @public
*/
export async function createWorkspace (db: Db, workspace: string, organisation: string): Promise<string> {
if ((await getWorkspace(db, workspace)) !== null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceAlreadyExists, { workspace }))
}
return await db
.collection(WORKSPACE_COLLECTION)
.insertOne({
workspace,
organisation
})
.then((e) => e.insertedId.toHexString())
}
async function getWorkspaceAndAccount (db: Db, email: string, workspace: string): Promise<{ accountId: ObjectId, workspaceId: ObjectId }> {
const wsPromise = await getWorkspace(db, workspace)
if (wsPromise === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
}
const workspaceId = wsPromise._id
const account = await getAccount(db, email)
if (account === null) {
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.AccountNotFound, { account: email }))
}
const accountId = account._id
return { accountId, workspaceId }
}
/**
* @public
*/
export async function assignWorkspace (db: Db, email: string, workspace: string): Promise<void> {
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, email, workspace)
// Add account into workspace.
await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $push: { accounts: accountId } })
// Add workspace to account
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: accountId }, { $push: { workspaces: workspaceId } })
}
/**
* @public
*/
export async function removeWorkspace (db: Db, email: string, workspace: string): Promise<void> {
const { workspaceId, accountId } = await getWorkspaceAndAccount(db, email, workspace)
// Add account into workspace.
await db.collection(WORKSPACE_COLLECTION).updateOne({ _id: workspaceId }, { $pull: { accounts: accountId } })
// Add account a workspace
await db.collection(ACCOUNT_COLLECTION).updateOne({ _id: accountId }, { $pull: { workspaces: workspaceId } })
}
function wrap (f: (db: Db, ...args: any[]) => Promise<any>) {
return async function (db: Db, request: Request<any[]>): Promise<Response<any>> {
return await f(db, ...request.params)
.then((result) => ({ id: request.id, result }))
.catch((err) => ({ error: unknownStatus(err) }))
}
}
/**
* @public
*/
export const methods = {
login: wrap(login),
getAccountInfo: wrap(getAccountInfo),
createAccount: wrap(createAccount),
createWorkspace: wrap(createWorkspace),
assignWorkspace: wrap(assignWorkspace),
removeWorkspace: wrap(removeWorkspace)
// updateAccount: wrap(updateAccount)
}
export default accountPlugin