Skip to content

Commit

Permalink
SIMSBIOHUB-117: Delete Artifact (#203)
Browse files Browse the repository at this point in the history
Added delete functionality to artifacts
  • Loading branch information
al-rosenthal authored Jun 29, 2023
1 parent 84811b3 commit 7190ce0
Show file tree
Hide file tree
Showing 17 changed files with 648 additions and 14 deletions.
130 changes: 130 additions & 0 deletions api/src/paths/artifact/delete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import OpenAPIRequestValidator, { OpenAPIRequestValidatorArgs } from 'openapi-request-validator';
import OpenAPIResponseValidator, { OpenAPIResponseValidatorArgs } from 'openapi-response-validator';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import * as db from '../../database/db';
import { ArtifactService } from '../../services/artifact-service';
import * as keycloakUtils from '../../utils/keycloak-utils';
import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db';
import { deleteArtifact, POST } from './delete';

chai.use(sinonChai);

describe('delete artifact', () => {
describe('openApiSchema', () => {
describe('request validation', () => {
const requestValidator = new OpenAPIRequestValidator(POST.apiDoc as unknown as OpenAPIRequestValidatorArgs);
it('should have property `artifactUUIDs`', async () => {
const request = {
headers: { 'content-type': 'application/json' },
body: {}
};
const response = requestValidator.validateRequest(request);
expect(response.status).to.equal(400);
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal("must have required property 'artifactUUIDs'");
});

it('should be an array error', async () => {
const request = {
headers: { 'content-type': 'application/json' },
body: {
artifactUUIDs: ''
}
};
const response = requestValidator.validateRequest(request);
expect(response.status).to.equal(400);
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal('must be array');
});

it('should match format "uuid" error', async () => {
const request = {
headers: { 'content-type': 'application/json' },
body: {
artifactUUIDs: ['uuid']
}
};
const response = requestValidator.validateRequest(request);
expect(response.status).to.equal(400);
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal('must match format "uuid"');
});
});

describe('response validation', () => {
const responseValidator = new OpenAPIResponseValidator(POST.apiDoc as unknown as OpenAPIResponseValidatorArgs);
describe('should throw an error', () => {
it('has null value', async () => {
const apiResponse = null;
const response = responseValidator.validateResponse(200, apiResponse);

expect(response.message).to.equal('The response was not valid.');
expect(response.errors[0].message).to.equal('must be boolean');
});

it('returning wrong response', async () => {
const apiResponse = { wrong_property: 1 };
const response = responseValidator.validateResponse(200, apiResponse);

expect(response.message).to.equal('The response was not valid.');
expect(response.errors[0].message).to.equal('must be boolean');
});
});

describe('responders properly', () => {
it('has valid values', async () => {
const apiResponse = true;
const response = responseValidator.validateResponse(200, apiResponse);

expect(response).to.equal(undefined);
});
});
});
});

describe('deleteArtifact', () => {
afterEach(() => {
sinon.restore();
});

it('catches and throws error', async () => {
const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() });
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);
sinon.stub(keycloakUtils, 'getKeycloakSource').resolves(false);
sinon.stub(ArtifactService.prototype, 'deleteArtifacts').throws('There was an issue deleting an artifact.');
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
mockReq.body = {
artifactUUIDs: ['ff84ecfc-046e-4cac-af59-a597047ce63d']
};
const requestHandler = deleteArtifact();

try {
await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (error: any) {
expect(error.name).to.be.eql('There was an issue deleting an artifact.');
expect(dbConnectionObj.release).to.have.been.calledOnce;
expect(dbConnectionObj.rollback).to.have.been.calledOnce;
}
});

it('responds with proper data', async () => {
const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() });
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);
sinon.stub(keycloakUtils, 'getKeycloakSource').resolves(false);
sinon.stub(ArtifactService.prototype, 'deleteArtifacts').resolves();
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
mockReq.body = {
artifactUUIDs: ['ff84ecfc-046e-4cac-af59-a597047ce63d']
};
const requestHandler = deleteArtifact();

await requestHandler(mockReq, mockRes, mockNext);
expect(dbConnectionObj.release).to.have.been.calledOnce;
expect(dbConnectionObj.rollback).to.have.not.been.calledOnce;
});
});
});
121 changes: 121 additions & 0 deletions api/src/paths/artifact/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SOURCE_SYSTEM } from '../../constants/database';
import { SYSTEM_ROLE } from '../../constants/roles';
import { getDBConnection, getServiceAccountDBConnection } from '../../database/db';
import { authorizeRequestHandler } from '../../request-handlers/security/authorization';
import { ArtifactService } from '../../services/artifact-service';
import { getKeycloakSource } from '../../utils/keycloak-utils';
import { getLogger } from '../../utils/logger';

const defaultLog = getLogger('paths/artifact/delete');

export const POST: Operation = [
authorizeRequestHandler(() => {
return {
or: [
{
validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR],
discriminator: 'SystemRole'
},
{
validServiceClientIDs: [SOURCE_SYSTEM['SIMS-SVC-4464']],
discriminator: 'ServiceClient'
}
]
};
}),
deleteArtifact()
];

POST.apiDoc = {
description: 'Deletes artifacts for a given list of UUIDs.',
tags: ['artifact'],
security: [
{
Bearer: []
}
],
requestBody: {
content: {
'application/json': {
schema: {
title: 'Artifacts to delete',
type: 'object',
required: ['artifactUUIDs'],
properties: {
artifactUUIDs: {
type: 'array',
items: {
type: 'string',
format: 'uuid'
},
minItems: 1
}
}
}
}
}
},
responses: {
200: {
description: '',
content: {
'application/json': {
schema: {
type: 'boolean',
description: 'A boolean indicating if the delete action was successful or not.'
}
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/403'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

/**
* Deletes artifacts for a given array of UUIDs.
* This will always respond with a JSON object {success: boolean} indicating
* if the artifacts have been successfully removed or not
*
* @returns {RequestHandler}
*/
export function deleteArtifact(): RequestHandler {
return async (req, res) => {
defaultLog.debug({ label: 'deleteArtifact', message: 'request body', req_body: req.query });

const sourceSystem = getKeycloakSource(req['keycloak_token']);
const connection = sourceSystem
? getServiceAccountDBConnection(sourceSystem)
: getDBConnection(req['keycloak_token']);

try {
await connection.open();
const service = new ArtifactService(connection);

await service.deleteArtifacts(req.body.artifactUUIDs);
res.status(200).json(true);
await connection.commit();
} catch (error: any) {
defaultLog.error({ label: 'deleteArtifact', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
5 changes: 3 additions & 2 deletions api/src/paths/artifact/intake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ describe('intake', () => {
}
});

it('throws an error when getKeycloakSource returns null', async () => {
it('throws error if getKeycloakSource returns null', async () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getServiceAccountDBConnection').returns(dbConnectionObj);

Expand All @@ -751,14 +751,15 @@ describe('intake', () => {

sinon.stub(fileUtils, 'scanFileForVirus').resolves(true);
sinon.stub(keycloakUtils, 'getKeycloakSource').returns(null);
const uploadStub = sinon.stub(ArtifactService.prototype, 'uploadAndPersistArtifact').resolves();

const requestHandler = intake.intakeArtifacts();

try {
await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as Error).message).to.equal('Failed to identify known submission source system');
expect(uploadStub).to.not.be.called;
}
});

Expand Down
5 changes: 3 additions & 2 deletions api/src/paths/dwc/submission/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('queue', () => {
}
});

it('throws an error when getKeycloakSource returns null', async () => {
it('throws error when getKeycloakSource returns null', async () => {
const dbConnectionObj = getMockDBConnection();
sinon.stub(db, 'getServiceAccountDBConnection').returns(dbConnectionObj);

Expand All @@ -348,14 +348,15 @@ describe('queue', () => {

sinon.stub(fileUtils, 'scanFileForVirus').resolves(true);
sinon.stub(keycloakUtils, 'getKeycloakSource').returns(null);

const intakeStub = sinon.stub(SubmissionJobQueueService.prototype, 'intake').resolves();
const requestHandler = queue.queueForProcess();

try {
await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as Error).message).to.equal('Failed to identify known submission source system');
expect(intakeStub).to.not.be.called;
}
});

Expand Down
32 changes: 32 additions & 0 deletions api/src/repositories/artifact-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,36 @@ describe('ArtifactRepository', () => {
expect(response).to.eql([]);
});
});

describe('getArtifactByUUID', () => {
it('should return with valid data', async () => {
const mockQueryResponse = {
rowCount: 1,
rows: [{ artifact_id: 1 }]
} as any as Promise<QueryResult<any>>;

const mockDBConnection = getMockDBConnection({
sql: () => mockQueryResponse
});
const artifactRepository = new ArtifactRepository(mockDBConnection);
const response = await artifactRepository.getArtifactByUUID('uuid');

expect(response).to.eql({ artifact_id: 1 });
});

it('should return null', async () => {
const mockQueryResponse = {
rowCount: 1,
rows: []
} as any as Promise<QueryResult<any>>;

const mockDBConnection = getMockDBConnection({
sql: () => mockQueryResponse
});
const artifactRepository = new ArtifactRepository(mockDBConnection);
const response = await artifactRepository.getArtifactByUUID('uuid');

expect(response).to.eql(null);
});
});
});
42 changes: 42 additions & 0 deletions api/src/repositories/artifact-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,32 @@ export class ArtifactRepository extends BaseRepository {
return result;
}

/**
* Retrieves all artifacts belonging to the given submission.
*
* @param {string} uuid
* @return {*} {Promise<Artifact>}
* @memberof ArtifactRepository
*/
async getArtifactByUUID(uuid: string): Promise<Artifact | null> {
defaultLog.debug({ label: 'getArtifactByUUID', uuid });

const sqlStatement = SQL`
SELECT
a.*
FROM
artifact a
WHERE
a.uuid = ${uuid};
`;

const response = await this.connection.sql<Artifact>(sqlStatement, Artifact);

const result = (response.rowCount && response?.rows[0]) || null;

return result;
}

/**
* Fetches multiple artifact records by the given artifact IDs
*
Expand Down Expand Up @@ -225,4 +251,20 @@ export class ArtifactRepository extends BaseRepository {
throw new ApiExecuteSQLError('Failed to update artifact security review timestamp');
}
}

/**
* Deletes a single artifact for a given UUID.
*
* @param uuid UUID of the artifact to delete
*/
async deleteArtifactByUUID(uuid: string): Promise<void> {
defaultLog.debug({ label: 'deleteArtifactByUUID' });

const sql = SQL`
DELETE
FROM artifact
WHERE uuid = ${uuid}
RETURNING *;`;
await this.connection.sql(sql);
}
}
Loading

0 comments on commit 7190ce0

Please sign in to comment.