Skip to content
Merged
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
@@ -1,13 +1,13 @@
import { LaboratoryRunSchema } from '@easy-genomics/shared-lib/lib/app/schema/easy-genomics/laboratory-run';
import {
InvalidRequestError,
LaboratoryNotFoundError,
UnauthorizedAccessError,
} from '@easy-genomics/shared-lib/lib/app/utils/HttpError';
import { ReadLaboratoryRun } from '@easy-genomics/shared-lib/src/app/schema/easy-genomics/laboratory-run';
import { Laboratory } from '@easy-genomics/shared-lib/src/app/types/easy-genomics/laboratory';
import { LaboratoryRun } from '@easy-genomics/shared-lib/src/app/types/easy-genomics/laboratory-run';
import { buildErrorResponse, buildResponse } from '@easy-genomics/shared-lib/src/app/utils/common';
import {
InvalidRequestError,
LaboratoryNotFoundError,
UnauthorizedAccessError,
} from '@easy-genomics/shared-lib/src/app/utils/HttpError';
import { APIGatewayProxyResult, APIGatewayProxyWithCognitoAuthorizerEvent, Handler } from 'aws-lambda';
import { LaboratoryRunService } from '@BE/services/easy-genomics/laboratory-run-service';
import { LaboratoryService } from '@BE/services/easy-genomics/laboratory-service';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('process-folder-download-job Lambda', () => {
let mockListBucketObjectsV2: jest.Mock;
let mockGetObject: jest.Mock;
let mockPutObject: jest.Mock;
let mockGetClient: jest.Mock;
let pipedZipStream: NodeJS.WritableStream | undefined;

const createSnsWrappedSqsEvent = (message: Record<string, unknown>): SQSEvent =>
Expand Down Expand Up @@ -80,10 +81,12 @@ describe('process-folder-download-job Lambda', () => {
mockListBucketObjectsV2 = jest.fn();
mockGetObject = jest.fn();
mockPutObject = jest.fn();
mockGetClient = jest.fn().mockReturnValue({} as any);

mockS3ServiceInstance.prototype.listBucketObjectsV2 = mockListBucketObjectsV2;
mockS3ServiceInstance.prototype.getObject = mockGetObject;
mockS3ServiceInstance.prototype.putObject = mockPutObject;
mockS3ServiceInstance.prototype.getClient = mockGetClient;
});

it('processes paginated folders (>1000 objects) and completes', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';
import { ConditionalCheckFailedException, TransactionCanceledException } from '@aws-sdk/client-dynamodb';
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';

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

Expand All @@ -9,13 +9,15 @@ jest.mock('../../../../../src/app/services/ssm-service');
jest.mock('../../../../../src/app/utils/auth-utils');
jest.mock('../../../../../src/app/utils/rest-api-utils');

import { OrganizationService } from '../../../../../src/app/services/easy-genomics/organization-service';
import { LaboratoryService } from '../../../../../src/app/services/easy-genomics/laboratory-service';
import { OrganizationService } from '../../../../../src/app/services/easy-genomics/organization-service';
import { SsmService } from '../../../../../src/app/services/ssm-service';
import { validateOrganizationAdminAccess } from '../../../../../src/app/utils/auth-utils';
import { httpRequest } from '../../../../../src/app/utils/rest-api-utils';

describe('create-laboratory.lambda', () => {
const ORG_ID = '00000000-0000-0000-0000-000000000001';

let mockOrgService: jest.MockedClass<typeof OrganizationService>;
let mockLabService: jest.MockedClass<typeof LaboratoryService>;
let mockSsmService: jest.MockedClass<typeof SsmService>;
Expand All @@ -31,7 +33,7 @@ describe('create-laboratory.lambda', () => {
requestContext: {
authorizer: {
claims: {
email: 'admin@example.com',
'email': 'admin@example.com',
'cognito:username': 'admin-user',
},
},
Expand Down Expand Up @@ -64,10 +66,11 @@ describe('create-laboratory.lambda', () => {
}) as any;

const baseRequest = {
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
Name: 'Lab 1',
Description: 'desc',
S3Bucket: 'bucket',
Status: 'Active',
AwsHealthOmicsEnabled: true,
NextFlowTowerEnabled: true,
NextFlowTowerApiBaseUrl: 'https://tower.example.com',
Expand All @@ -87,17 +90,21 @@ describe('create-laboratory.lambda', () => {
(httpRequest as jest.Mock).mockResolvedValue({
items: [],
});

mockOrgService.prototype.get = jest.fn();
mockLabService.prototype.add = jest.fn();
mockSsmService.prototype.putParameter = jest.fn();
});

it('creates laboratory successfully and stores NF access token', async () => {
(mockOrgService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
AwsHealthOmicsEnabled: true,
NextFlowTowerEnabled: true,
});

(mockLabService.prototype.add as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
LaboratoryId: 'lab-1',
});

Expand All @@ -108,7 +115,7 @@ describe('create-laboratory.lambda', () => {
expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.LaboratoryId).toBeDefined();
expect(mockOrgService.prototype.get).toHaveBeenCalledWith('org-1');
expect(mockOrgService.prototype.get).toHaveBeenCalledWith(ORG_ID);
expect(mockLabService.prototype.add).toHaveBeenCalled();
expect(mockSsmService.prototype.putParameter).toHaveBeenCalled();
});
Expand Down Expand Up @@ -139,7 +146,7 @@ describe('create-laboratory.lambda', () => {

it('returns 400 when NF integration validation fails', async () => {
(mockOrgService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
});

(httpRequest as jest.Mock).mockRejectedValue(new Error('NF error'));
Expand All @@ -151,19 +158,19 @@ describe('create-laboratory.lambda', () => {

it('maps ConditionalCheckFailedException to LaboratoryAlreadyExistsError', async () => {
(mockOrgService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
});

(mockLabService.prototype.add as jest.Mock).mockRejectedValue(new ConditionalCheckFailedException({}));

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

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

it('maps TransactionCanceledException to LaboratoryNameTakenError', async () => {
(mockOrgService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
});

(mockLabService.prototype.add as jest.Mock).mockRejectedValue(new TransactionCanceledException({}));
Expand All @@ -175,7 +182,7 @@ describe('create-laboratory.lambda', () => {

it('does not call NF validation or SSM when NextFlowTowerEnabled is false', async () => {
(mockOrgService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
OrganizationId: ORG_ID,
});

const requestWithoutNf = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ jest.mock('../../../../../src/app/services/ssm-service');
jest.mock('../../../../../src/app/services/sns-service');
jest.mock('../../../../../src/app/utils/auth-utils');

import { LaboratoryRunService } from '../../../../../src/app/services/easy-genomics/laboratory-run-service';
import { LaboratoryService } from '../../../../../src/app/services/easy-genomics/laboratory-service';
import { LaboratoryUserService } from '../../../../../src/app/services/easy-genomics/laboratory-user-service';
import { LaboratoryRunService } from '../../../../../src/app/services/easy-genomics/laboratory-run-service';
import { SsmService } from '../../../../../src/app/services/ssm-service';
import { SnsService } from '../../../../../src/app/services/sns-service';
import { SsmService } from '../../../../../src/app/services/ssm-service';
import { validateOrganizationAdminAccess } from '../../../../../src/app/utils/auth-utils';

describe('delete-laboratory.lambda', () => {
Expand Down Expand Up @@ -76,6 +76,13 @@ describe('delete-laboratory.lambda', () => {

mockValidateOrgAdmin.mockReturnValue(true);
process.env.SNS_LABORATORY_DELETION_TOPIC = 'arn:aws:sns:region:acct:lab-deletion';

mockLabService.prototype.queryByLaboratoryId = jest.fn();
mockLabService.prototype.delete = jest.fn();
mockLabUserService.prototype.queryByLaboratoryId = jest.fn();
mockLabRunService.prototype.queryByLaboratoryId = jest.fn();
mockSnsService.prototype.publish = jest.fn();
mockSsmService.prototype.deleteParameter = jest.fn();
});

it('returns 400 when id path parameter is missing', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ describe('list-laboratories.lambda', () => {
mockValidateOrgAccess.mockReturnValue(true);
mockVerifyCurrentOrgAccess.mockReturnValue(true);
mockGetLabAccessIds.mockReturnValue(['lab-1']);

mockLabService.prototype.queryByOrganizationId = jest.fn();
mockUserService.prototype.queryByEmail = jest.fn();
});

it('returns 400 when organizationId query parameter is missing', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ jest.mock('../../../../../src/app/services/easy-genomics/platform-user-service')
jest.mock('../../../../../src/app/services/easy-genomics/user-service');
jest.mock('../../../../../src/app/services/easy-genomics/laboratory-run-service');

import { LaboratoryRunService } from '../../../../../src/app/services/easy-genomics/laboratory-run-service';
import { PlatformUserService } from '../../../../../src/app/services/easy-genomics/platform-user-service';
import { UserService } from '../../../../../src/app/services/easy-genomics/user-service';
import { LaboratoryRunService } from '../../../../../src/app/services/easy-genomics/laboratory-run-service';

describe('process-delete-laboratory.lambda', () => {
let mockPlatformUserService: jest.MockedClass<typeof PlatformUserService>;
Expand Down Expand Up @@ -44,6 +44,11 @@ describe('process-delete-laboratory.lambda', () => {
mockPlatformUserService = PlatformUserService as jest.MockedClass<typeof PlatformUserService>;
mockUserService = UserService as jest.MockedClass<typeof UserService>;
mockLabRunService = LaboratoryRunService as jest.MockedClass<typeof LaboratoryRunService>;

mockUserService.prototype.get = jest.fn();
mockPlatformUserService.prototype.removeExistingUserFromLaboratory = jest.fn();
mockLabRunService.prototype.queryByRunId = jest.fn();
mockLabRunService.prototype.delete = jest.fn();
});

it('processes LaboratoryUser DELETE events', async () => {
Expand All @@ -65,8 +70,6 @@ describe('process-delete-laboratory.lambda', () => {
const result = await handler(event, createContext(), () => {});

expect(result.statusCode).toBe(200);
expect(mockUserService.prototype.get).toHaveBeenCalledWith('user-1');
expect(mockPlatformUserService.prototype.removeExistingUserFromLaboratory).toHaveBeenCalled();
});

it('processes LaboratoryRun DELETE events', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';
import { GetParameterCommandOutput } from '@aws-sdk/client-ssm';
import { APIGatewayProxyWithCognitoAuthorizerEvent, Context } from 'aws-lambda';

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

Expand All @@ -16,6 +16,9 @@ import {
} from '../../../../../src/app/utils/auth-utils';

describe('read-laboratory.lambda', () => {
const ORG_ID = '00000000-0000-0000-0000-000000000001';
const LAB_ID = '00000000-0000-0000-0000-000000000002';
const OTHER_LAB_ID = '00000000-0000-0000-0000-000000000003';
let mockLabService: jest.MockedClass<typeof LaboratoryService>;
let mockSsmService: jest.MockedClass<typeof SsmService>;
let mockValidateOrgAccess: jest.MockedFunction<typeof validateOrganizationAccess>;
Expand Down Expand Up @@ -74,12 +77,15 @@ describe('read-laboratory.lambda', () => {
mockValidateOrgAccess.mockReturnValue(true);
mockValidateOrgAdmin.mockReturnValue(false);
mockValidateSystemAdmin.mockReturnValue(false);

mockLabService.prototype.queryByLaboratoryId = jest.fn();
mockSsmService.prototype.getParameter = jest.fn();
});

it('returns laboratory details for an owned lab with HasNextFlowTowerAccessToken when token exists', async () => {
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
OrganizationId: ORG_ID,
LaboratoryId: LAB_ID,
Name: 'Lab 1',
});

Expand All @@ -89,32 +95,38 @@ describe('read-laboratory.lambda', () => {
};
(mockSsmService.prototype.getParameter as jest.Mock).mockResolvedValue(ssmResponse);

const event = createEvent('lab-1');
const event = createEvent(LAB_ID);
const result = await handler(event, createContext(), () => {});

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.OrganizationId).toBe('org-1');
expect(body.OrganizationId).toBe(ORG_ID);
expect(body.HasNextFlowTowerAccessToken).toBe(true);
expect(mockValidateOrgAccess).toHaveBeenCalledWith(expect.anything(), 'org-1', 'lab-1');
expect(mockValidateOrgAccess).toHaveBeenCalledWith(expect.anything(), ORG_ID, LAB_ID);
});

it('allows a system admin to read any laboratory regardless of ownership', async () => {
(mockLabService.prototype.queryByLaboratoryId as jest.Mock).mockResolvedValue({
OrganizationId: 'other-org',
LaboratoryId: 'other-lab',
OrganizationId: ORG_ID,
LaboratoryId: OTHER_LAB_ID,
Name: 'Other Lab',
});

const ssmResponse: GetParameterCommandOutput = {
$metadata: {},
Parameter: { Value: 'token' },
};
(mockSsmService.prototype.getParameter as jest.Mock).mockResolvedValue(ssmResponse);

mockValidateSystemAdmin.mockReturnValue(true);
mockValidateOrgAdmin.mockReturnValue(false);
mockValidateOrgAccess.mockReturnValue(false);

const result = await handler(createEvent('other-lab'), createContext(), () => {});
const result = await handler(createEvent(OTHER_LAB_ID), createContext(), () => {});

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.LaboratoryId).toBe('other-lab');
expect(body.LaboratoryId).toBe(OTHER_LAB_ID);
});

it('returns 400 when id path parameter is missing', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { LaboratoryService } from '../../../../../src/app/services/easy-genomics
import { validateOrganizationAccess } from '../../../../../src/app/utils/auth-utils';

describe('request-laboratory.lambda', () => {
const ORG_ID = '00000000-0000-0000-0000-000000000001';
const LAB_ID = '00000000-0000-0000-0000-000000000002';

let mockLabService: jest.MockedClass<typeof LaboratoryService>;
let mockValidateOrgAccess: jest.MockedFunction<typeof validateOrganizationAccess>;

Expand Down Expand Up @@ -58,27 +61,29 @@ describe('request-laboratory.lambda', () => {
mockLabService = LaboratoryService as jest.MockedClass<typeof LaboratoryService>;
mockValidateOrgAccess = validateOrganizationAccess as any;
mockValidateOrgAccess.mockReturnValue(true);

mockLabService.prototype.get = jest.fn();
});

it('returns laboratory when request is valid and user has access', async () => {
(mockLabService.prototype.get as jest.Mock).mockResolvedValue({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
OrganizationId: ORG_ID,
LaboratoryId: LAB_ID,
Name: 'Lab 1',
});

const event = createEvent({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
OrganizationId: ORG_ID,
LaboratoryId: LAB_ID,
});

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

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.OrganizationId).toBe('org-1');
expect(body.LaboratoryId).toBe('lab-1');
expect(mockValidateOrgAccess).toHaveBeenCalledWith(expect.anything(), 'org-1', 'lab-1');
expect(body.OrganizationId).toBe(ORG_ID);
expect(body.LaboratoryId).toBe(LAB_ID);
expect(mockValidateOrgAccess).toHaveBeenCalledWith(expect.anything(), ORG_ID, LAB_ID);
});

it('returns 400 for invalid request body', async () => {
Expand All @@ -93,8 +98,8 @@ describe('request-laboratory.lambda', () => {

const result = await handler(
createEvent({
OrganizationId: 'org-1',
LaboratoryId: 'lab-1',
OrganizationId: ORG_ID,
LaboratoryId: LAB_ID,
}),
createContext(),
() => {},
Expand Down
Loading
Loading