From d875f573ad8e4ab6f8eb85240d1a86f64edb6b50 Mon Sep 17 00:00:00 2001 From: Siyao Wang Date: Tue, 18 Jun 2024 16:25:24 +0100 Subject: [PATCH] feat(trpc-role): Add tRPC role backend --- packages/itmat-cores/src/trpcCore/dataCore.ts | 18 +- .../src/trpcCore/permissionCore.ts | 209 ++++++++++- .../itmat-cores/src/trpcCore/studyCore.ts | 8 +- .../itmat-interface/src/trpc/roleProcedure.ts | 119 +++++++ .../itmat-interface/src/trpc/tRPCRouter.ts | 4 +- .../test/trpcTests/role.test.ts | 336 ++++++++++++++++++ 6 files changed, 674 insertions(+), 20 deletions(-) create mode 100644 packages/itmat-interface/src/trpc/roleProcedure.ts create mode 100644 packages/itmat-interface/test/trpcTests/role.test.ts diff --git a/packages/itmat-cores/src/trpcCore/dataCore.ts b/packages/itmat-cores/src/trpcCore/dataCore.ts index 6ca9c4574..ae5ba58a9 100644 --- a/packages/itmat-cores/src/trpcCore/dataCore.ts +++ b/packages/itmat-cores/src/trpcCore/dataCore.ts @@ -77,7 +77,7 @@ export class TRPCDataCore { enumCoreErrors.NOT_LOGGED_IN ); } - const roles = await this.permissionCore.getRolesOfUser(requester, studyId); + const roles = await this.permissionCore.getRolesOfUser(requester, requester.id, studyId); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -163,7 +163,7 @@ export class TRPCDataCore { ); } - const roles = await this.permissionCore.getRolesOfUser(requester, fieldInput.studyId); + const roles = await this.permissionCore.getRolesOfUser(requester, requester.id, fieldInput.studyId); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -279,7 +279,7 @@ export class TRPCDataCore { ); } - const roles = await this.permissionCore.getRolesOfUser(requester, fieldInput.studyId); + const roles = await this.permissionCore.getRolesOfUser(requester, requester.id, fieldInput.studyId); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -395,7 +395,7 @@ export class TRPCDataCore { ); } - const roles = await this.permissionCore.getRolesOfUser(requester, studyId); + const roles = await this.permissionCore.getRolesOfUser(requester, requester.id, studyId); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -745,7 +745,7 @@ export class TRPCDataCore { enumCoreErrors.NOT_LOGGED_IN ); } - const roles = (await this.permissionCore.getRolesOfUser(requester, studyId)); + const roles = (await this.permissionCore.getRolesOfUser(requester, requester.id, studyId)); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -864,7 +864,7 @@ export class TRPCDataCore { enumCoreErrors.NOT_LOGGED_IN ); } - const roles = (await this.permissionCore.getRolesOfUser(requester, studyId)); + const roles = (await this.permissionCore.getRolesOfUser(requester, requester.id, studyId)); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -957,7 +957,7 @@ export class TRPCDataCore { enumCoreErrors.NOT_LOGGED_IN ); } - const roles = (await this.permissionCore.getRolesOfUser(requester, studyId)); + const roles = (await this.permissionCore.getRolesOfUser(requester, requester.id, studyId)); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -1168,7 +1168,7 @@ export class TRPCDataCore { ); } - const roles = await this.permissionCore.getRolesOfUser(requester, studyId); + const roles = await this.permissionCore.getRolesOfUser(requester, requester.id, studyId); if (roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -1224,7 +1224,7 @@ export class TRPCDataCore { ); } - const roles = await this.permissionCore.getRolesOfUser(requester, studyId); + const roles = await this.permissionCore.getRolesOfUser(requester, requester.id, studyId); if (requester.type !== enumUserTypes.ADMIN && roles.length === 0) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, diff --git a/packages/itmat-cores/src/trpcCore/permissionCore.ts b/packages/itmat-cores/src/trpcCore/permissionCore.ts index 48a771c60..fcf64a2ab 100644 --- a/packages/itmat-cores/src/trpcCore/permissionCore.ts +++ b/packages/itmat-cores/src/trpcCore/permissionCore.ts @@ -1,5 +1,7 @@ -import { IData, IField, IUserWithoutToken, enumDataAtomicPermissions, permissionString } from '@itmat-broker/itmat-types'; +import { CoreError, IData, IDataPermission, IField, IRole, IUserWithoutToken, enumCoreErrors, enumDataAtomicPermissions, enumStudyRoles, enumUserTypes, permissionString } from '@itmat-broker/itmat-types'; import { DBType } from '../database/database'; +import { v4 as uuid } from 'uuid'; +import { makeGenericReponse } from '../utils'; export class TRPCPermissionCore { db: DBType; @@ -12,11 +14,62 @@ export class TRPCPermissionCore { * * @param user * @param studyId - * @returns + * + * @returns - IRole[] */ - public async getRolesOfUser(user: IUserWithoutToken, studyId?: string) { - return studyId ? await this.db.collections.roles_collection.find({ 'studyId': studyId, 'users': user.id, 'life.deletedTime': null }).toArray() : - await this.db.collections.roles_collection.find({ 'users': user.id, 'life.deletedTime': null }).toArray(); + public async getRolesOfUser(requester: IUserWithoutToken | undefined, userId: string, studyId?: string) { + if (!requester) { + throw new CoreError( + enumCoreErrors.NOT_LOGGED_IN, + enumCoreErrors.NOT_LOGGED_IN + ); + } + // if studyId is provided, only admins, study managers and user him/herself can see the roles + // if studyId is not provided, only admins and user him/herself can see the roles + if (studyId) { + const requesterRoles = await this.db.collections.roles_collection.find({ 'studyId': studyId, 'users': requester.id, 'life.deletedTime': null }).toArray(); + if (requester.type !== enumUserTypes.ADMIN && requesterRoles.every(role => role.studyRole !== enumStudyRoles.STUDY_MANAGER) && requester.id !== userId) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + return await this.db.collections.roles_collection.find({ 'studyId': studyId, 'users': userId, 'life.deletedTime': null }).toArray(); + } else { + if (requester.type !== enumUserTypes.ADMIN && requester.id !== userId) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + return await this.db.collections.roles_collection.find({ 'users': userId, 'life.deletedTime': null }).toArray(); + } + } + + /** + * Get the roles of a study. + * + * @param requester - The requester. + * @param studyId - The id of the study. + * + * @returns - IRole[] + */ + public async getRolesOfStudy(requester: IUserWithoutToken | undefined, studyId: string) { + if (!requester) { + throw new CoreError( + enumCoreErrors.NOT_LOGGED_IN, + enumCoreErrors.NOT_LOGGED_IN + ); + } + const roles = await this.getRolesOfUser(requester, requester.id, studyId); + if (requester.type !== enumUserTypes.ADMIN && roles.every(role => role.studyRole !== enumStudyRoles.STUDY_MANAGER)) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + + return await this.db.collections.roles_collection.find({ 'studyId': studyId, 'life.deletedTime': null }).toArray(); } /** @@ -30,7 +83,7 @@ export class TRPCPermissionCore { * @returns boolean */ public async checkFieldOrDataPermission(user: IUserWithoutToken, studyId: string, entry: Partial | Partial, permission: enumDataAtomicPermissions) { - const roles = await this.getRolesOfUser(user, studyId); + const roles = await this.getRolesOfUser(user, user.id, studyId); for (const role of roles) { const dataPermissions = role.dataPermissions; for (const dataPermission of dataPermissions) { @@ -57,4 +110,148 @@ export class TRPCPermissionCore { } return true; } + + /** + * Create a new role. + * + * @param requester - The requester. + * @param studyId - The id of the study. + * @param name - The name of the role. + * @param description - The description of the role. + * @param dataPermissions - The data permissions of the role. + * @param studyRole - The role of the study. + * @param users - The users of the role. + * @returns IRole + */ + public async createStudyRole(requester: IUserWithoutToken | undefined, studyId: string, name: string, description?: string, dataPermissions?: IDataPermission[], studyRole?: enumStudyRoles, users?: string[]) { + if (!requester) { + throw new CoreError( + enumCoreErrors.NOT_LOGGED_IN, + enumCoreErrors.NOT_LOGGED_IN + ); + } + + const roles = await this.getRolesOfUser(requester, requester.id, studyId); + if (requester.type !== enumUserTypes.ADMIN && roles.every(role => role.studyRole !== enumStudyRoles.STUDY_MANAGER)) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + + const newRole: IRole = { + id: uuid(), + studyId: studyId, + name: name, + description: description, + dataPermissions: dataPermissions ?? [{ + fields: [], + dataProperties: {}, + includeUnVersioned: false, + permission: 0 + }], + studyRole: studyRole ?? enumStudyRoles.STUDY_USER, + users: users ?? [], + life: { + createdTime: Date.now(), + createdUser: requester.id, + deletedTime: null, + deletedUser: null + }, + metadata: {} + }; + + await this.db.collections.roles_collection.insertOne(newRole); + return newRole; + } + + /** + * Edit a role. + * + * @param requester - The requester. + * @param roleId - The id of the role. + * @param name - The name of the role. + * @param description - The description of the role. + * @param dataPermissions - The data permissions of the role. + * @param studyRole - The role of the study. + * @param users - The users of the role. + * + * @returns IRole + */ + public async editStudyRole(requester: IUserWithoutToken | undefined, roleId: string, name?: string, description?: string, dataPermissions?: IDataPermission[], studyRole?: enumStudyRoles, users?: string[]) { + if (!requester) { + throw new CoreError( + enumCoreErrors.NOT_LOGGED_IN, + enumCoreErrors.NOT_LOGGED_IN + ); + } + + const role = await this.db.collections.roles_collection.findOne({ 'id': roleId, 'life.deletedTime': null }); + if (!role) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + const roles = await this.getRolesOfUser(requester, requester.id, role.studyId); + if (requester.type !== enumUserTypes.ADMIN && roles.every(role => role.studyRole !== enumStudyRoles.STUDY_MANAGER)) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + + const res = await this.db.collections.roles_collection.findOneAndUpdate({ id: roleId }, { + $set: { + name: name ?? role.name, + description: description ?? role.description, + dataPermissions: dataPermissions ?? role.dataPermissions, + studyRole: studyRole ?? role.studyRole, + users: users ?? role.users + } + }, { + returnDocument: 'after' + }); + return res; + } + + /** + * Delete a role. + * + * @param requester - The requester. + * @param roleId - The id of the role. + * @returns - IGenericResponse + */ + public async deleteStudyRole(requester: IUserWithoutToken | undefined, roleId: string) { + if (!requester) { + throw new CoreError( + enumCoreErrors.NOT_LOGGED_IN, + enumCoreErrors.NOT_LOGGED_IN + ); + } + + const role = await this.db.collections.roles_collection.findOne({ 'id': roleId, 'life.deletedTime': null }); + if (!role) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + const roles = await this.getRolesOfUser(requester, requester.id, role.studyId); + if (requester.type !== enumUserTypes.ADMIN && roles.every(role => role.studyRole !== enumStudyRoles.STUDY_MANAGER)) { + throw new CoreError( + enumCoreErrors.NO_PERMISSION_ERROR, + enumCoreErrors.NO_PERMISSION_ERROR + ); + } + + await this.db.collections.roles_collection.findOneAndUpdate({ id: roleId }, { + $set: { + 'life.deletedTime': Date.now(), + 'life.deletedUser': requester.id + } + }); + + return makeGenericReponse(roleId, true, undefined, 'Role deleted successfully.'); + } } \ No newline at end of file diff --git a/packages/itmat-cores/src/trpcCore/studyCore.ts b/packages/itmat-cores/src/trpcCore/studyCore.ts index ab82ded4f..00c0e1f7a 100644 --- a/packages/itmat-cores/src/trpcCore/studyCore.ts +++ b/packages/itmat-cores/src/trpcCore/studyCore.ts @@ -37,7 +37,7 @@ export class TRPCStudyCore { return studyId ? await this.db.collections.studies_collection.find({ 'id': studyId, 'life.deletedTime': null }).toArray() : await this.db.collections.studies_collection.find({ 'life.deletedTime': null }).toArray(); } - const roleStudyIds = (await this.permissionCore.getRolesOfUser(requester)).map(role => role.studyId); + const roleStudyIds = (await this.permissionCore.getRolesOfUser(requester, requester.id)).map(role => role.studyId); const query: Filter = { 'life.deletedTime': null }; if (studyId) { @@ -179,7 +179,7 @@ export class TRPCStudyCore { enumCoreErrors.NOT_LOGGED_IN ); } - const roleStudyRoles: enumStudyRoles[] = (await this.permissionCore.getRolesOfUser(requester, studyId)).map(role => role.studyRole); + const roleStudyRoles: enumStudyRoles[] = (await this.permissionCore.getRolesOfUser(requester, requester.id, studyId)).map(role => role.studyRole); if (requester.type !== enumUserTypes.ADMIN && !roleStudyRoles.includes(enumStudyRoles.STUDY_MANAGER)) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -307,7 +307,7 @@ export class TRPCStudyCore { ); } - const roleStudyRoles: enumStudyRoles[] = (await this.permissionCore.getRolesOfUser(requester)).map(role => role.studyRole); + const roleStudyRoles: enumStudyRoles[] = (await this.permissionCore.getRolesOfUser(requester, requester.id)).map(role => role.studyRole); if (requester.type !== enumUserTypes.ADMIN && !roleStudyRoles.includes(enumStudyRoles.STUDY_MANAGER)) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, @@ -448,7 +448,7 @@ export class TRPCStudyCore { } /* check privileges */ - const roleStudyRoles: enumStudyRoles[] = (await this.permissionCore.getRolesOfUser(requester)).map(role => role.studyRole); + const roleStudyRoles: enumStudyRoles[] = (await this.permissionCore.getRolesOfUser(requester, requester.id)).map(role => role.studyRole); if (requester.type !== enumUserTypes.ADMIN && !roleStudyRoles.includes(enumStudyRoles.STUDY_MANAGER)) { throw new CoreError( enumCoreErrors.NO_PERMISSION_ERROR, diff --git a/packages/itmat-interface/src/trpc/roleProcedure.ts b/packages/itmat-interface/src/trpc/roleProcedure.ts new file mode 100644 index 000000000..4038a9106 --- /dev/null +++ b/packages/itmat-interface/src/trpc/roleProcedure.ts @@ -0,0 +1,119 @@ +import { z } from 'zod'; +import { baseProcedure, router } from './trpc'; +import { TRPCPermissionCore } from '@itmat-broker/itmat-cores'; +import { db } from '../database/database'; +import { enumStudyRoles } from '@itmat-broker/itmat-types'; + +const permissionCore = new TRPCPermissionCore(db); + +export const roleRouter = router({ + /** + * Get the roles of a user. + * + * @param studyId - The id of the study. + * + * @returns IRole[] + */ + getUserRoles: baseProcedure.input(z.object({ + userId: z.string(), + studyId: z.optional(z.string()) + })).query(async (opts) => { + return await permissionCore.getRolesOfUser(opts.ctx.user, opts.input.userId, opts.input.studyId); + }), + /** + * Get the roles of a study. + * + * @param studyId - The id of the study. + * + * @returns IRole[] + */ + getStudyRoles: baseProcedure.input(z.object({ + studyId: z.string() + })).query(async (opts) => { + return await permissionCore.getRolesOfStudy(opts.ctx.user, opts.input.studyId); + }), + /** + * Create a new study role. + * + * @param studyId - The id of the study. + * @param name - The name of the role. + * @param description - The description of the role. + * @param dataPermissions - The data permissions for the role. + * @param studyRole - The role of the study. + * @param users - The users of the role. + * + * @returns IRole + */ + createStudyRole: baseProcedure.input(z.object({ + studyId: z.string(), + name: z.string(), + description: z.optional(z.string().optional()), + dataPermissions: z.optional(z.array(z.object({ + fields: z.array(z.string()), + dataProperties: z.record(z.array(z.string())), + includeUnVersioned: z.boolean(), + permission: z.number() + }))), + studyRole: z.optional(z.nativeEnum(enumStudyRoles)), + users: z.optional(z.array(z.string())) + })).mutation(async (opts) => { + return await permissionCore.createStudyRole( + opts.ctx.user, + opts.input.studyId, + opts.input.name, + opts.input.description, + opts.input.dataPermissions, + opts.input.studyRole, + opts.input.users + ); + }), + /** + * Edit a study role. + * + * @param roleId - The id of the role. + * @param name - The name of the role. + * @param description - The description of the role. + * @param dataPermissions - The data permissions for the role. + * @param studyRole - The role of the study. + * @param users - The users of the role. + * + * @returns IRole + */ + editStudyRole: baseProcedure.input(z.object({ + roleId: z.string(), + name: z.optional(z.string()), + description: z.optional(z.string().optional()), + dataPermissions: z.optional(z.array(z.object({ + fields: z.array(z.string()), + dataProperties: z.record(z.array(z.string())), + includeUnVersioned: z.boolean(), + permission: z.number() + }))), + studyRole: z.optional(z.nativeEnum(enumStudyRoles)), + users: z.optional(z.array(z.string())) + })).mutation(async (opts) => { + return await permissionCore.editStudyRole( + opts.ctx.user, + opts.input.roleId, + opts.input.name, + opts.input.description, + opts.input.dataPermissions, + opts.input.studyRole, + opts.input.users + ); + }), + /** + * Delete a study role. + * + * @param roleId - The id of the role. + * + * @returns IRole + */ + deleteStudyRole: baseProcedure.input(z.object({ + roleId: z.string() + })).mutation(async (opts) => { + return await permissionCore.deleteStudyRole(opts.ctx.user, opts.input.roleId); + }) +}); + + diff --git a/packages/itmat-interface/src/trpc/tRPCRouter.ts b/packages/itmat-interface/src/trpc/tRPCRouter.ts index 146f7ae34..f97450c09 100644 --- a/packages/itmat-interface/src/trpc/tRPCRouter.ts +++ b/packages/itmat-interface/src/trpc/tRPCRouter.ts @@ -1,5 +1,6 @@ import { dataRouter } from './dataProcedure'; import { driveRouter } from './driveProcedure'; +import { roleRouter } from './roleProcedure'; import { studyRouter } from './studyProcedure'; import { router } from './trpc'; import { userRouter } from './userProcedure'; @@ -8,7 +9,8 @@ export const tRPCRouter = router({ user: userRouter, drive: driveRouter, study: studyRouter, - data: dataRouter + data: dataRouter, + role: roleRouter }); export type APPTRPCRouter = typeof tRPCRouter; \ No newline at end of file diff --git a/packages/itmat-interface/test/trpcTests/role.test.ts b/packages/itmat-interface/test/trpcTests/role.test.ts new file mode 100644 index 000000000..6c957eed3 --- /dev/null +++ b/packages/itmat-interface/test/trpcTests/role.test.ts @@ -0,0 +1,336 @@ +/** + * @with Minio + */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { db } from '../../src/database/database'; +import { Express } from 'express'; +import { objStore } from '../../src/objStore/objStore'; +import request from 'supertest'; +import { connectAdmin, connectUser, connectAgent } from './_loginHelper'; +import { Router } from '../../src/server/router'; +import { MongoClient } from 'mongodb'; +import { setupDatabase } from '@itmat-broker/itmat-setup'; +import config from '../../config/config.sample.json'; +import { v4 as uuid } from 'uuid'; +import { enumUserTypes, enumStudyRoles, enumCoreErrors } from '@itmat-broker/itmat-types'; +import { encodeQueryParams } from './helper'; + +if (global.hasMinio) { + let app: Express; + let mongodb: MongoMemoryServer; + let admin: request.SuperTest; + let user: request.SuperTest; + let authorisedUser: request.SuperTest; + let mongoConnection: MongoClient; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let mongoClient: Db; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let adminProfile; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let userProfile; + let authorisedUserProfile; + let study; + let fullPermissionRole: IRole; + + afterAll(async () => { + await db.closeConnection(); + await mongoConnection?.close(); + await mongodb.stop(); + + /* claer all mocks */ + jest.clearAllMocks(); + }); + + beforeAll(async () => { // eslint-disable-line no-undef + /* Creating a in-memory MongoDB instance for testing */ + const dbName = uuid(); + mongodb = await MongoMemoryServer.create({ instance: { dbName } }); + const connectionString = mongodb.getUri(); + await setupDatabase(connectionString, dbName); + /* Wiring up the backend server */ + config.objectStore.port = (global as any).minioContainerPort; + config.database.mongo_url = connectionString; + config.database.database = dbName; + await db.connect(config.database, MongoClient); + await objStore.connect(config.objectStore); + const router = new Router(config); + await router.init(); + /* Connect mongo client (for test setup later / retrieve info later) */ + mongoConnection = await MongoClient.connect(connectionString); + mongoClient = mongoConnection.db(dbName); + + /* Connecting clients for testing later */ + app = router.getApp(); + admin = request.agent(app); + user = request.agent(app); + await connectAdmin(admin); + await connectUser(user); + + // add the root node for each user + const users = await db.collections.users_collection.find({}).toArray(); + adminProfile = users.filter(el => el.type === enumUserTypes.ADMIN)[0]; + userProfile = users.filter(el => el.type === enumUserTypes.STANDARD)[0]; + + const username = uuid(); + authorisedUserProfile = { + username, + type: enumUserTypes.STANDARD, + firstname: `${username}_firstname`, + lastname: `${username}_lastname`, + password: '$2b$04$j0aSK.Dyq7Q9N.r6d0uIaOGrOe7sI4rGUn0JNcaXcPCv.49Otjwpi', + otpSecret: 'H6BNKKO27DPLCATGEJAZNWQV4LWOTMRA', + email: `${username}@example.com`, + description: 'I am a new user.', + emailNotificationsActivated: true, + organisation: 'organisation_system', + id: `new_user_id_${username}`, + emailNotificationsStatus: { expiringNotification: false }, + resetPasswordRequests: [], + expiredAt: 1991134065000, + life: { + createdTime: 1591134065000, + createdUser: 'admin', + deletedTime: null, + deletedUser: null + }, + metadata: {} + }; + await mongoClient.collection(config.database.collections.users_collection).insertOne(authorisedUserProfile); + authorisedUser = request.agent(app); + await connectAgent(authorisedUser, username, 'admin', authorisedUserProfile.otpSecret); + }); + + beforeEach(async () => { + study = { + id: uuid(), + name: 'Test Study', + currentDataVersion: -1, // index; dataVersions[currentDataVersion] gives current version; // -1 if no data + dataVersions: [], + description: null, + profile: null, + webLayout: [], + life: { + createdTime: 0, + createdUser: enumUserTypes.SYSTEM, + deletedTime: null, + deletedUser: null + }, + metadata: {} + }; + await db.collections.studies_collection.insertOne(study); + fullPermissionRole = { + id: 'full_permission_role_id', + studyId: study.id, + name: 'Full Permissison Role', + description: '', + // data permissions for studyId + dataPermissions: [{ + fields: ['^1.*$'], + dataProperties: { + }, + permission: parseInt('111', 2), + includeUnVersioned: true + }], + studyRole: enumStudyRoles.STUDY_MANAGER, + users: [authorisedUserProfile.id], + groups: [], + life: { + createdTime: 0, + createdUser: 'admin', + deletedTime: null, + deletedUser: null + }, + metadata: {} + }; + await db.collections.roles_collection.insertOne(fullPermissionRole); + }); + + afterEach(async () => { + await db.collections.studies_collection.deleteMany({}); + await db.collections.field_dictionary_collection.deleteMany({}); + await db.collections.data_collection.deleteMany({}); + await db.collections.roles_collection.deleteMany({}); + await db.collections.files_collection.deleteMany({}); + await db.collections.cache_collection.deleteMany({}); + await db.collections.configs_collection.deleteMany({}); + }); + + describe('tRPC data APIs', () => { + test('getUserRoles (admin)', async () => { + const parameters = { userId: authorisedUserProfile.id }; + const response = await admin.get('/trpc/role.getUserRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(1); + expect(response.body.result.data[0].id).toBe(fullPermissionRole.id); + }); + test('getUserRoles (authorised user)', async () => { + const parameters = { userId: authorisedUserProfile.id }; + const response = await authorisedUser.get('/trpc/role.getUserRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(1); + expect(response.body.result.data[0].id).toBe(fullPermissionRole.id); + }); + test('getUserRoles (with studyId)', async () => { + const parameters = { userId: authorisedUserProfile.id, studyId: study.id }; + const response = await authorisedUser.get('/trpc/role.getUserRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(1); + expect(response.body.result.data[0].id).toBe(fullPermissionRole.id); + }); + test('getUserRoles (with incorrect study Id)', async () => { + const parameters = { userId: authorisedUserProfile.id, studyId: 'incorrect' }; + const response = await authorisedUser.get('/trpc/role.getUserRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(0); + }); + test('getUserRoles (unauthorised user)', async () => { + const parameters = { userId: authorisedUserProfile.id }; + const response = await user.get('/trpc/role.getUserRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + test('getStudyRoles (admin)', async () => { + const parameters = { studyId: study.id }; + const response = await admin.get('/trpc/role.getStudyRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(1); + expect(response.body.result.data[0].id).toBe(fullPermissionRole.id); + }); + test('getStudyRoles (authorised user)', async () => { + const parameters = { studyId: study.id }; + const response = await authorisedUser.get('/trpc/role.getStudyRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(200); + expect(response.body.result.data).toHaveLength(1); + expect(response.body.result.data[0].id).toBe(fullPermissionRole.id); + }); + test('getStudyRoles (unauthorised user)', async () => { + const parameters = { studyId: study.id }; + const response = await user.get('/trpc/role.getStudyRoles?input=' + encodeQueryParams(parameters)); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + test('createStudyRole (admin)', async () => { + const response = await admin.post('/trpc/role.createStudyRole').send({ + studyId: study.id, + name: 'New Role', + description: 'New Role Description', + dataPermissions: [{ + fields: ['^1.*$'], + dataProperties: {}, + permission: parseInt('111', 2), + includeUnVersioned: true + }], + studyRole: enumStudyRoles.STUDY_MANAGER, + users: [authorisedUserProfile.id] + }); + expect(response.status).toBe(200); + const roleInDb = await db.collections.roles_collection.findOne({ name: 'New Role' }); + expect(roleInDb?.id).toBe(response.body.result.data.id); + }); + test('createStudyRole (authorised user)', async () => { + const response = await authorisedUser.post('/trpc/role.createStudyRole').send({ + studyId: study.id, + name: 'New Role', + description: 'New Role Description', + dataPermissions: [{ + fields: ['^1.*$'], + dataProperties: {}, + permission: parseInt('111', 2), + includeUnVersioned: true + }], + studyRole: enumStudyRoles.STUDY_MANAGER, + users: [authorisedUserProfile.id] + }); + expect(response.status).toBe(200); + const roleInDb = await db.collections.roles_collection.findOne({ name: 'New Role' }); + expect(roleInDb?.id).toBe(response.body.result.data.id); + }); + test('createStudyRole (unauthorised user)', async () => { + const response = await user.post('/trpc/role.createStudyRole').send({ + studyId: study.id, + name: 'New Role', + description: 'New Role Description', + dataPermissions: [{ + fields: ['^1.*$'], + dataProperties: {}, + permission: parseInt('111', 2), + includeUnVersioned: true + }], + studyRole: enumStudyRoles.STUDY_MANAGER, + users: [authorisedUserProfile.id] + }); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + test('editStudyRole (admin)', async () => { + const response = await admin.post('/trpc/role.editStudyRole').send({ + roleId: fullPermissionRole.id, + name: 'Edited Role Name' + }); + expect(response.status).toBe(200); + const roleInDb = await db.collections.roles_collection.findOne({ id: fullPermissionRole.id }); + expect(roleInDb?.name).toBe('Edited Role Name'); + }); + test('editStudyRole (authorised user)', async () => { + const response = await authorisedUser.post('/trpc/role.editStudyRole').send({ + roleId: fullPermissionRole.id, + name: 'Edited Role Name' + }); + expect(response.status).toBe(200); + const roleInDb = await db.collections.roles_collection.findOne({ id: fullPermissionRole.id }); + expect(roleInDb?.name).toBe('Edited Role Name'); + }); + test('editStudyRole (unauthorised user)', async () => { + const response = await user.post('/trpc/role.editStudyRole').send({ + roleId: fullPermissionRole.id, + name: 'Edited Role Name' + }); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + test('editStudyRole (invalid roleId)', async () => { + const response = await user.post('/trpc/role.editStudyRole').send({ + roleId: 'random', + name: 'Edited Role Name' + }); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + test('deleteStudyRole (admin)', async () => { + const response = await admin.post('/trpc/role.deleteStudyRole').send({ + roleId: fullPermissionRole.id + }); + expect(response.status).toBe(200); + const roleInDb = await db.collections.roles_collection.findOne({ id: fullPermissionRole.id }); + expect(roleInDb?.life.deletedTime).not.toBeNull(); + }); + test('deleteStudyRole (authorised user)', async () => { + const response = await authorisedUser.post('/trpc/role.deleteStudyRole').send({ + roleId: fullPermissionRole.id + }); + expect(response.status).toBe(200); + const roleInDb = await db.collections.roles_collection.findOne({ id: fullPermissionRole.id }); + expect(roleInDb?.life.deletedTime).not.toBeNull(); + }); + test('deleteStudyRole (unauthorised user)', async () => { + const response = await user.post('/trpc/role.deleteStudyRole').send({ + roleId: fullPermissionRole.id + }); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + test('deleteStudyRole (invalid roleId)', async () => { + const response = await user.post('/trpc/role.deleteStudyRole').send({ + roleId: 'random' + }); + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(enumCoreErrors.NO_PERMISSION_ERROR); + }); + }); +} else { + test(`${__filename.split(/[\\/]/).pop()} skipped because it requires Minio on Docker`, () => { + expect(true).toBe(true); + }); +} \ No newline at end of file