Skip to content

Commit 04292a3

Browse files
committed
feat: added unit tests for laboratory users lambda functions
1 parent 2ca49fe commit 04292a3

8 files changed

Lines changed: 953 additions & 2 deletions

File tree

packages/back-end/src/app/controllers/easy-genomics/laboratory/user/list-laboratory-users-details.lambda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const handler: Handler = async (
5555
}
5656
};
5757

58-
const listLaboratoryUsers = (
58+
export const listLaboratoryUsers = (
5959
organizationId?: string,
6060
laboratoryId?: string,
6161
userId?: string,

packages/back-end/src/app/controllers/easy-genomics/laboratory/user/list-laboratory-users.lambda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const handler: Handler = async (
2929
}
3030
};
3131

32-
const listLaboratoryUsers = (
32+
export const listLaboratoryUsers = (
3333
organizationId?: string,
3434
laboratoryId?: string,
3535
userId?: string,
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';
2+
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
3+
4+
import { handler } from '../../../../../../src/app/controllers/easy-genomics/laboratory/user/add-laboratory-user.lambda';
5+
6+
jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-service');
7+
jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-user-service');
8+
jest.mock('../../../../../../src/app/services/easy-genomics/organization-user-service');
9+
jest.mock('../../../../../../src/app/services/easy-genomics/platform-user-service');
10+
jest.mock('../../../../../../src/app/services/easy-genomics/user-service');
11+
jest.mock('../../../../../../src/app/utils/auth-utils');
12+
13+
import { LaboratoryService } from '../../../../../../src/app/services/easy-genomics/laboratory-service';
14+
import { LaboratoryUserService } from '../../../../../../src/app/services/easy-genomics/laboratory-user-service';
15+
import { OrganizationUserService } from '../../../../../../src/app/services/easy-genomics/organization-user-service';
16+
import { PlatformUserService } from '../../../../../../src/app/services/easy-genomics/platform-user-service';
17+
import { UserService } from '../../../../../../src/app/services/easy-genomics/user-service';
18+
import {
19+
validateLaboratoryManagerAccess,
20+
validateOrganizationAdminAccess,
21+
} from '../../../../../../src/app/utils/auth-utils';
22+
23+
describe('add-laboratory-user.lambda', () => {
24+
let mockLabService: jest.MockedClass<typeof LaboratoryService>;
25+
let mockLabUserService: jest.MockedClass<typeof LaboratoryUserService>;
26+
let mockOrgUserService: jest.MockedClass<typeof OrganizationUserService>;
27+
let mockPlatformUserService: jest.MockedClass<typeof PlatformUserService>;
28+
let mockUserService: jest.MockedClass<typeof UserService>;
29+
let mockValidateOrgAdmin: jest.MockedFunction<typeof validateOrganizationAdminAccess>;
30+
let mockValidateLabManager: jest.MockedFunction<typeof validateLaboratoryManagerAccess>;
31+
32+
const createEvent = (body: any, overrides: Partial<APIGatewayProxyWithCognitoAuthorizerEvent> = {}) =>
33+
({
34+
body: JSON.stringify(body),
35+
isBase64Encoded: false,
36+
httpMethod: 'POST',
37+
path: '/laboratory/user/add',
38+
headers: {},
39+
requestContext: {
40+
authorizer: {
41+
claims: {
42+
email: 'admin@example.com',
43+
'cognito:username': 'admin-user',
44+
},
45+
},
46+
},
47+
resource: '',
48+
queryStringParameters: null,
49+
multiValueQueryStringParameters: null,
50+
pathParameters: null,
51+
stageVariables: null,
52+
multiValueHeaders: {},
53+
...overrides,
54+
}) as any;
55+
56+
const createContext = (): Context =>
57+
({
58+
functionName: 'add-laboratory-user',
59+
functionVersion: '$LATEST',
60+
invokedFunctionArn: 'arn:aws:lambda:region:acct:function:add-laboratory-user',
61+
memoryLimitInMB: '128',
62+
awsRequestId: 'req-id',
63+
logGroupName: '/aws/lambda/add-laboratory-user',
64+
logStreamName: '2026/03/11/[$LATEST]test',
65+
identity: undefined,
66+
clientContext: undefined,
67+
callbackWaitsForEmptyEventLoop: true,
68+
getRemainingTimeInMillis: () => 30000,
69+
done: jest.fn(),
70+
fail: jest.fn(),
71+
succeed: jest.fn(),
72+
}) as any;
73+
74+
const baseRequest = {
75+
OrganizationId: 'org-1',
76+
LaboratoryId: 'lab-1',
77+
UserId: 'user-1',
78+
LabManager: true,
79+
LabTechnician: false,
80+
} as any;
81+
82+
beforeEach(() => {
83+
jest.clearAllMocks();
84+
mockLabService = LaboratoryService as jest.MockedClass<typeof LaboratoryService>;
85+
mockLabUserService = LaboratoryUserService as jest.MockedClass<typeof LaboratoryUserService>;
86+
mockOrgUserService = OrganizationUserService as jest.MockedClass<typeof OrganizationUserService>;
87+
mockPlatformUserService = PlatformUserService as jest.MockedClass<typeof PlatformUserService>;
88+
mockUserService = UserService as jest.MockedClass<typeof UserService>;
89+
mockValidateOrgAdmin = validateOrganizationAdminAccess as any;
90+
mockValidateLabManager = validateLaboratoryManagerAccess as any;
91+
92+
mockValidateOrgAdmin.mockReturnValue(true);
93+
mockValidateLabManager.mockReturnValue(false);
94+
});
95+
96+
it('adds existing user to laboratory when caller is org admin and mapping does not exist', async () => {
97+
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
98+
OrganizationId: 'org-1',
99+
LaboratoryId: 'lab-1',
100+
});
101+
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
102+
UserId: 'user-1',
103+
});
104+
(mockOrgUserService.prototype.get as jest.Mock).mockResolvedValue({
105+
OrganizationId: 'org-1',
106+
UserId: 'user-1',
107+
});
108+
(mockLabUserService.prototype.get as jest.Mock).mockRejectedValueOnce(
109+
// simulate LaboratoryUserNotFoundError, which should be swallowed
110+
new (class extends Error {})(),
111+
);
112+
(mockPlatformUserService.prototype.addExistingUserToLaboratory as jest.Mock).mockResolvedValue(true);
113+
114+
const result = await handler(createEvent(baseRequest), createContext(), () => {});
115+
116+
expect(result.statusCode).toBe(200);
117+
const body = JSON.parse(result.body);
118+
expect(body.Status).toBe('Success');
119+
expect(mockPlatformUserService.prototype.addExistingUserToLaboratory).toHaveBeenCalled();
120+
});
121+
122+
it('rejects invalid request body', async () => {
123+
const result = await handler(createEvent({}), createContext(), () => {});
124+
125+
expect(result.statusCode).toBe(400);
126+
expect(mockPlatformUserService.prototype.addExistingUserToLaboratory).not.toHaveBeenCalled();
127+
});
128+
129+
it('denies access when caller is neither org admin nor lab manager', async () => {
130+
mockValidateOrgAdmin.mockReturnValue(false);
131+
mockValidateLabManager.mockReturnValue(false);
132+
133+
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
134+
OrganizationId: 'org-1',
135+
LaboratoryId: 'lab-1',
136+
});
137+
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
138+
UserId: 'user-1',
139+
});
140+
141+
const result = await handler(createEvent(baseRequest), createContext(), () => {});
142+
143+
expect(result.statusCode).toBe(403);
144+
});
145+
146+
it('returns error when user already has laboratory access mapping', async () => {
147+
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
148+
OrganizationId: 'org-1',
149+
LaboratoryId: 'lab-1',
150+
});
151+
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
152+
UserId: 'user-1',
153+
});
154+
(mockOrgUserService.prototype.get as jest.Mock).mockResolvedValue({
155+
OrganizationId: 'org-1',
156+
UserId: 'user-1',
157+
});
158+
(mockLabUserService.prototype.get as jest.Mock).mockResolvedValue({
159+
LaboratoryId: 'lab-1',
160+
UserId: 'user-1',
161+
});
162+
163+
const result = await handler(createEvent(baseRequest), createContext(), () => {});
164+
165+
expect(result.statusCode).toBe(409);
166+
});
167+
168+
it('maps ConditionalCheckFailedException from platformUserService to LaboratoryUserAlreadyExistsError', async () => {
169+
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
170+
OrganizationId: 'org-1',
171+
LaboratoryId: 'lab-1',
172+
});
173+
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
174+
UserId: 'user-1',
175+
});
176+
(mockOrgUserService.prototype.get as jest.Mock).mockResolvedValue({
177+
OrganizationId: 'org-1',
178+
UserId: 'user-1',
179+
});
180+
(mockLabUserService.prototype.get as jest.Mock).mockRejectedValueOnce(new (class extends Error {})());
181+
(mockPlatformUserService.prototype.addExistingUserToLaboratory as jest.Mock).mockRejectedValue(
182+
new ConditionalCheckFailedException({}),
183+
);
184+
185+
const result = await handler(createEvent(baseRequest), createContext(), () => {});
186+
187+
expect(result.statusCode).toBe(409);
188+
});
189+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';
2+
3+
import { handler } from '../../../../../../src/app/controllers/easy-genomics/laboratory/user/edit-laboratory-user.lambda';
4+
5+
jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-service');
6+
jest.mock('../../../../../../src/app/services/easy-genomics/laboratory-user-service');
7+
jest.mock('../../../../../../src/app/services/easy-genomics/platform-user-service');
8+
jest.mock('../../../../../../src/app/services/easy-genomics/user-service');
9+
jest.mock('../../../../../../src/app/utils/auth-utils');
10+
11+
import { LaboratoryService } from '../../../../../../src/app/services/easy-genomics/laboratory-service';
12+
import { LaboratoryUserService } from '../../../../../../src/app/services/easy-genomics/laboratory-user-service';
13+
import { PlatformUserService } from '../../../../../../src/app/services/easy-genomics/platform-user-service';
14+
import { UserService } from '../../../../../../src/app/services/easy-genomics/user-service';
15+
import {
16+
validateLaboratoryManagerAccess,
17+
validateOrganizationAdminAccess,
18+
} from '../../../../../../src/app/utils/auth-utils';
19+
20+
describe('edit-laboratory-user.lambda', () => {
21+
let mockLabService: jest.MockedClass<typeof LaboratoryService>;
22+
let mockLabUserService: jest.MockedClass<typeof LaboratoryUserService>;
23+
let mockPlatformUserService: jest.MockedClass<typeof PlatformUserService>;
24+
let mockUserService: jest.MockedClass<typeof UserService>;
25+
let mockValidateOrgAdmin: jest.MockedFunction<typeof validateOrganizationAdminAccess>;
26+
let mockValidateLabManager: jest.MockedFunction<typeof validateLaboratoryManagerAccess>;
27+
28+
const createEvent = (body: any, overrides: Partial<APIGatewayProxyWithCognitoAuthorizerEvent> = {}) =>
29+
({
30+
body: JSON.stringify(body),
31+
isBase64Encoded: false,
32+
httpMethod: 'PUT',
33+
path: '/laboratory/user/edit',
34+
headers: {},
35+
requestContext: {
36+
authorizer: {
37+
claims: {
38+
email: 'admin@example.com',
39+
'cognito:username': 'admin-user',
40+
},
41+
},
42+
},
43+
resource: '',
44+
queryStringParameters: null,
45+
multiValueQueryStringParameters: null,
46+
pathParameters: null,
47+
stageVariables: null,
48+
multiValueHeaders: {},
49+
...overrides,
50+
}) as any;
51+
52+
const createContext = (): Context =>
53+
({
54+
functionName: 'edit-laboratory-user',
55+
functionVersion: '$LATEST',
56+
invokedFunctionArn: 'arn:aws:lambda:region:acct:function:edit-laboratory-user',
57+
memoryLimitInMB: '128',
58+
awsRequestId: 'req-id',
59+
logGroupName: '/aws/lambda/edit-laboratory-user',
60+
logStreamName: '2026/03/11/[$LATEST]test',
61+
identity: undefined,
62+
clientContext: undefined,
63+
callbackWaitsForEmptyEventLoop: true,
64+
getRemainingTimeInMillis: () => 30000,
65+
done: jest.fn(),
66+
fail: jest.fn(),
67+
succeed: jest.fn(),
68+
}) as any;
69+
70+
const baseRequest = {
71+
OrganizationId: 'org-1',
72+
LaboratoryId: 'lab-1',
73+
UserId: 'user-1',
74+
LabManager: false,
75+
LabTechnician: true,
76+
} as any;
77+
78+
beforeEach(() => {
79+
jest.clearAllMocks();
80+
mockLabService = LaboratoryService as jest.MockedClass<typeof LaboratoryService>;
81+
mockLabUserService = LaboratoryUserService as jest.MockedClass<typeof LaboratoryUserService>;
82+
mockPlatformUserService = PlatformUserService as jest.MockedClass<typeof PlatformUserService>;
83+
mockUserService = UserService as jest.MockedClass<typeof UserService>;
84+
mockValidateOrgAdmin = validateOrganizationAdminAccess as any;
85+
mockValidateLabManager = validateLaboratoryManagerAccess as any;
86+
87+
mockValidateOrgAdmin.mockReturnValue(true);
88+
mockValidateLabManager.mockReturnValue(false);
89+
});
90+
91+
it('edits existing laboratory user mapping when caller has access', async () => {
92+
(mockLabUserService.prototype.get as jest.Mock).mockResolvedValue({
93+
LaboratoryId: 'lab-1',
94+
UserId: 'user-1',
95+
LabManager: true,
96+
LabTechnician: false,
97+
});
98+
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
99+
OrganizationId: 'org-1',
100+
LaboratoryId: 'lab-1',
101+
});
102+
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
103+
UserId: 'user-1',
104+
});
105+
(mockPlatformUserService.prototype.editExistingUserAccessToLaboratory as jest.Mock).mockResolvedValue(true);
106+
107+
const result = await handler(createEvent(baseRequest), createContext(), () => {});
108+
109+
expect(result.statusCode).toBe(200);
110+
const body = JSON.parse(result.body);
111+
expect(body.Status).toBe('Success');
112+
expect(mockPlatformUserService.prototype.editExistingUserAccessToLaboratory).toHaveBeenCalled();
113+
});
114+
115+
it('rejects invalid request body', async () => {
116+
const result = await handler(createEvent({}), createContext(), () => {});
117+
118+
expect(result.statusCode).toBe(400);
119+
expect(mockPlatformUserService.prototype.editExistingUserAccessToLaboratory).not.toHaveBeenCalled();
120+
});
121+
122+
it('denies access when caller is neither org admin nor lab manager', async () => {
123+
(mockLabUserService.prototype.get as jest.Mock).mockResolvedValue({
124+
LaboratoryId: 'lab-1',
125+
UserId: 'user-1',
126+
});
127+
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
128+
OrganizationId: 'org-1',
129+
LaboratoryId: 'lab-1',
130+
});
131+
(mockUserService.prototype.get as jest.Mock).mockResolvedValue({
132+
UserId: 'user-1',
133+
});
134+
135+
mockValidateOrgAdmin.mockReturnValue(false);
136+
mockValidateLabManager.mockReturnValue(false);
137+
138+
const result = await handler(createEvent(baseRequest), createContext(), () => {});
139+
140+
expect(result.statusCode).toBe(403);
141+
});
142+
});

0 commit comments

Comments
 (0)