Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const handler: Handler = async (
}
};

const listLaboratoryUsers = (
export const listLaboratoryUsers = (
organizationId?: string,
laboratoryId?: string,
userId?: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const handler: Handler = async (
}
};

const listLaboratoryUsers = (
export const listLaboratoryUsers = (
organizationId?: string,
laboratoryId?: string,
userId?: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';

import { handler } from '../../../../../../src/app/controllers/easy-genomics/laboratory/user/add-laboratory-user.lambda';

jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-service');
jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-user-service');
jest.mock('../../../../../../src/app/services/easy-genomics/organization-user-service');
jest.mock('../../../../../../src/app/services/easy-genomics/platform-user-service');
jest.mock('../../../../../../src/app/services/easy-genomics/user-service');
jest.mock('../../../../../../src/app/utils/auth-utils');

import { LaboratoryService } from '../../../../../../src/app/services/easy-genomics/laboratory-service';
import { LaboratoryUserService } from '../../../../../../src/app/services/easy-genomics/laboratory-user-service';
import { OrganizationUserService } from '../../../../../../src/app/services/easy-genomics/organization-user-service';
import { PlatformUserService } from '../../../../../../src/app/services/easy-genomics/platform-user-service';
import { UserService } from '../../../../../../src/app/services/easy-genomics/user-service';
import {
validateLaboratoryManagerAccess,
validateOrganizationAdminAccess,
} from '../../../../../../src/app/utils/auth-utils';

describe('add-laboratory-user.lambda', () => {
let mockLabService: jest.MockedClass<typeof LaboratoryService>;
let mockLabUserService: jest.MockedClass<typeof LaboratoryUserService>;
let mockOrgUserService: jest.MockedClass<typeof OrganizationUserService>;
let mockPlatformUserService: jest.MockedClass<typeof PlatformUserService>;
let mockUserService: jest.MockedClass<typeof UserService>;
let mockValidateOrgAdmin: jest.MockedFunction<typeof validateOrganizationAdminAccess>;
let mockValidateLabManager: jest.MockedFunction<typeof validateLaboratoryManagerAccess>;

const createEvent = (body: any, overrides: Partial<APIGatewayProxyWithCognitoAuthorizerEvent> = {}) =>
({
body: JSON.stringify(body),
isBase64Encoded: false,
httpMethod: 'POST',
path: '/laboratory/user/add',
headers: {},
requestContext: {
authorizer: {
claims: {
email: 'admin@example.com',
'cognito:username': 'admin-user',
},
},
},
resource: '',
queryStringParameters: null,
multiValueQueryStringParameters: null,
pathParameters: null,
stageVariables: null,
multiValueHeaders: {},
...overrides,
}) as any;

const createContext = (): Context =>
({
functionName: 'add-laboratory-user',
functionVersion: '$LATEST',
invokedFunctionArn: 'arn:aws:lambda:region:acct:function:add-laboratory-user',
memoryLimitInMB: '128',
awsRequestId: 'req-id',
logGroupName: '/aws/lambda/add-laboratory-user',
logStreamName: '2026/03/11/[$LATEST]test',
identity: undefined,
clientContext: undefined,
callbackWaitsForEmptyEventLoop: true,
getRemainingTimeInMillis: () => 30000,
done: jest.fn(),
fail: jest.fn(),
succeed: jest.fn(),
}) as any;

const baseRequest = {
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
UserId: 'user-1',
LabManager: true,
LabTechnician: false,
} as any;

beforeEach(() => {
jest.clearAllMocks();
mockLabService = LaboratoryService as jest.MockedClass<typeof LaboratoryService>;
mockLabUserService = LaboratoryUserService as jest.MockedClass<typeof LaboratoryUserService>;
mockOrgUserService = OrganizationUserService as jest.MockedClass<typeof OrganizationUserService>;
mockPlatformUserService = PlatformUserService as jest.MockedClass<typeof PlatformUserService>;
mockUserService = UserService as jest.MockedClass<typeof UserService>;
mockValidateOrgAdmin = validateOrganizationAdminAccess as any;
mockValidateLabManager = validateLaboratoryManagerAccess as any;

mockValidateOrgAdmin.mockReturnValue(true);
mockValidateLabManager.mockReturnValue(false);
});

it('adds existing user to laboratory when caller is org admin and mapping does not exist', async () => {
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
});
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
UserId: 'user-1',
});
(mockOrgUserService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
UserId: 'user-1',
});
(mockLabUserService.prototype.get as jest.Mock).mockRejectedValueOnce(
// simulate LaboratoryUserNotFoundError, which should be swallowed
new (class extends Error {})(),
);
(mockPlatformUserService.prototype.addExistingUserToLaboratory as jest.Mock).mockResolvedValue(true);

const result = await handler(createEvent(baseRequest), createContext(), () => {});

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.Status).toBe('Success');
expect(mockPlatformUserService.prototype.addExistingUserToLaboratory).toHaveBeenCalled();
});

it('rejects invalid request body', async () => {
const result = await handler(createEvent({}), createContext(), () => {});

expect(result.statusCode).toBe(400);
expect(mockPlatformUserService.prototype.addExistingUserToLaboratory).not.toHaveBeenCalled();
});

it('denies access when caller is neither org admin nor lab manager', async () => {
mockValidateOrgAdmin.mockReturnValue(false);
mockValidateLabManager.mockReturnValue(false);

(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
});
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
UserId: 'user-1',
});

const result = await handler(createEvent(baseRequest), createContext(), () => {});

expect(result.statusCode).toBe(403);
});

it('returns error when user already has laboratory access mapping', async () => {
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
});
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
UserId: 'user-1',
});
(mockOrgUserService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
UserId: 'user-1',
});
(mockLabUserService.prototype.get as jest.Mock).mockResolvedValue({
LaboratoryId: 'lab-1',
UserId: 'user-1',
});

const result = await handler(createEvent(baseRequest), createContext(), () => {});

expect(result.statusCode).toBe(409);
});

it('maps ConditionalCheckFailedException from platformUserService to LaboratoryUserAlreadyExistsError', async () => {
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
});
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
UserId: 'user-1',
});
(mockOrgUserService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
UserId: 'user-1',
});
(mockLabUserService.prototype.get as jest.Mock).mockRejectedValueOnce(new (class extends Error {})());
(mockPlatformUserService.prototype.addExistingUserToLaboratory as jest.Mock).mockRejectedValue(
new ConditionalCheckFailedException({}),
);

const result = await handler(createEvent(baseRequest), createContext(), () => {});

expect(result.statusCode).toBe(409);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';

import { handler } from '../../../../../../src/app/controllers/easy-genomics/laboratory/user/edit-laboratory-user.lambda';

jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-service');
jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-user-service');
jest.mock('../../../../../../src/app/services/easy-genomics/platform-user-service');
jest.mock('../../../../../../src/app/services/easy-genomics/user-service');
jest.mock('../../../../../../src/app/utils/auth-utils');

import { LaboratoryService } from '../../../../../../src/app/services/easy-genomics/laboratory-service';
import { LaboratoryUserService } from '../../../../../../src/app/services/easy-genomics/laboratory-user-service';
import { PlatformUserService } from '../../../../../../src/app/services/easy-genomics/platform-user-service';
import { UserService } from '../../../../../../src/app/services/easy-genomics/user-service';
import {
validateLaboratoryManagerAccess,
validateOrganizationAdminAccess,
} from '../../../../../../src/app/utils/auth-utils';

describe('edit-laboratory-user.lambda', () => {
let mockLabService: jest.MockedClass<typeof LaboratoryService>;
let mockLabUserService: jest.MockedClass<typeof LaboratoryUserService>;
let mockPlatformUserService: jest.MockedClass<typeof PlatformUserService>;
let mockUserService: jest.MockedClass<typeof UserService>;
let mockValidateOrgAdmin: jest.MockedFunction<typeof validateOrganizationAdminAccess>;
let mockValidateLabManager: jest.MockedFunction<typeof validateLaboratoryManagerAccess>;

const createEvent = (body: any, overrides: Partial<APIGatewayProxyWithCognitoAuthorizerEvent> = {}) =>
({
body: JSON.stringify(body),
isBase64Encoded: false,
httpMethod: 'PUT',
path: '/laboratory/user/edit',
headers: {},
requestContext: {
authorizer: {
claims: {
email: 'admin@example.com',
'cognito:username': 'admin-user',
},
},
},
resource: '',
queryStringParameters: null,
multiValueQueryStringParameters: null,
pathParameters: null,
stageVariables: null,
multiValueHeaders: {},
...overrides,
}) as any;

const createContext = (): Context =>
({
functionName: 'edit-laboratory-user',
functionVersion: '$LATEST',
invokedFunctionArn: 'arn:aws:lambda:region:acct:function:edit-laboratory-user',
memoryLimitInMB: '128',
awsRequestId: 'req-id',
logGroupName: '/aws/lambda/edit-laboratory-user',
logStreamName: '2026/03/11/[$LATEST]test',
identity: undefined,
clientContext: undefined,
callbackWaitsForEmptyEventLoop: true,
getRemainingTimeInMillis: () => 30000,
done: jest.fn(),
fail: jest.fn(),
succeed: jest.fn(),
}) as any;

const baseRequest = {
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
UserId: 'user-1',
LabManager: false,
LabTechnician: true,
} as any;

beforeEach(() => {
jest.clearAllMocks();
mockLabService = LaboratoryService as jest.MockedClass<typeof LaboratoryService>;
mockLabUserService = LaboratoryUserService as jest.MockedClass<typeof LaboratoryUserService>;
mockPlatformUserService = PlatformUserService as jest.MockedClass<typeof PlatformUserService>;
mockUserService = UserService as jest.MockedClass<typeof UserService>;
mockValidateOrgAdmin = validateOrganizationAdminAccess as any;
mockValidateLabManager = validateLaboratoryManagerAccess as any;

mockValidateOrgAdmin.mockReturnValue(true);
mockValidateLabManager.mockReturnValue(false);
});

it('edits existing laboratory user mapping when caller has access', async () => {
(mockLabUserService.prototype.get as jest.Mock).mockResolvedValue({
LaboratoryId: 'lab-1',
UserId: 'user-1',
LabManager: true,
LabTechnician: false,
});
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
});
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
UserId: 'user-1',
});
(mockPlatformUserService.prototype.editExistingUserAccessToLaboratory as jest.Mock).mockResolvedValue(true);

const result = await handler(createEvent(baseRequest), createContext(), () => {});

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.Status).toBe('Success');
expect(mockPlatformUserService.prototype.editExistingUserAccessToLaboratory).toHaveBeenCalled();
});

it('rejects invalid request body', async () => {
const result = await handler(createEvent({}), createContext(), () => {});

expect(result.statusCode).toBe(400);
expect(mockPlatformUserService.prototype.editExistingUserAccessToLaboratory).not.toHaveBeenCalled();
});

it('denies access when caller is neither org admin nor lab manager', async () => {
(mockLabUserService.prototype.get as jest.Mock).mockResolvedValue({
LaboratoryId: 'lab-1',
UserId: 'user-1',
});
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
});
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
UserId: 'user-1',
});

mockValidateOrgAdmin.mockReturnValue(false);
mockValidateLabManager.mockReturnValue(false);

const result = await handler(createEvent(baseRequest), createContext(), () => {});

expect(result.statusCode).toBe(403);
});
});
Loading
Loading