From 40950b5ca9da53ae89cd3b307c0a9f3f663effbd Mon Sep 17 00:00:00 2001 From: Andrey Platov Date: Thu, 9 Sep 2021 22:04:00 +0200 Subject: [PATCH] account tests Signed-off-by: Andrey Platov --- server/account/src/__tests__/account.test.ts | 157 +++++++++++++++++++ server/account/src/index.ts | 139 +++++++++++++++- 2 files changed, 288 insertions(+), 8 deletions(-) create mode 100644 server/account/src/__tests__/account.test.ts diff --git a/server/account/src/__tests__/account.test.ts b/server/account/src/__tests__/account.test.ts new file mode 100644 index 0000000000..f1b487dcfc --- /dev/null +++ b/server/account/src/__tests__/account.test.ts @@ -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() + }) +}) diff --git a/server/account/src/index.ts b/server/account/src/index.ts index 9f35632483..6110555ad2 100644 --- a/server/account/src/index.ts +++ b/server/account/src/index.ts @@ -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 +/** + * @public + */ +export type AccountInfo = Omit 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 { +/** + * @public + */ +export async function getAccount (db: Db, email: string): Promise { return await db.collection(ACCOUNT_COLLECTION).findOne({ email }) } -async function getWorkspace (db: Db, workspace: string): Promise { +/** + * @public + * @param db - + * @param workspace - + * @returns + */ +export async function getWorkspace (db: Db, workspace: string): Promise { return await db.collection(WORKSPACE_COLLECTION).findOne({ 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 { + 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 { + 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 { + 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 { + 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) { + return async function (db: Db, request: Request): Promise> { + 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