Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BHBC 1289: Soft Delete an Uploaded Observation #462

Merged
merged 7 commits into from
Aug 20, 2021
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
Expand Up @@ -115,14 +115,19 @@ export function getOccurenceSubmission(): RequestHandler {
getOccurrenceSubmissionSQLStatement.values
);

let messageList = [];

// Ensure we only retrieve the latest occurrence submission record if it has not been soft deleted
if (
occurrenceSubmissionData &&
occurrenceSubmissionData.rows &&
occurrenceSubmissionData.rows[0] &&
occurrenceSubmissionData.rows[0].submission_status_type_name === 'Rejected'
!occurrenceSubmissionData ||
!occurrenceSubmissionData.rows ||
!occurrenceSubmissionData.rows[0] ||
occurrenceSubmissionData.rows[0].soft_delete_timestamp
) {
return res.status(200).json(null);
}

let messageList = [];

if (occurrenceSubmissionData.rows[0].submission_status_type_name === 'Rejected') {
const occurrence_submission_id = occurrenceSubmissionData.rows[0].id;
const getSubmissionErrorListSQLStatement = getOccurrenceSubmissionMessagesSQL(Number(occurrence_submission_id));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import * as delete_submission from './delete';
import * as db from '../../../../../../../../database/db';
import * as survey_occurrence_queries from '../../../../../../../../queries/survey/survey-occurrence-queries';
import SQL from 'sql-template-strings';

chai.use(sinonChai);

describe('deleteOccurrenceSubmission', () => {
const dbConnectionObj = {
systemUserId: () => {
return null;
},
open: async () => {
// do nothing
},
release: () => {
// do nothing
},
commit: async () => {
// do nothing
},
rollback: async () => {
// do nothing
},
query: async () => {
// do nothing
}
};

const sampleReq = {
keycloak_token: {},
params: {
projectId: 1,
surveyId: 1,
submissionId: 1
}
} as any;

let actualResult: any = null;

const sampleRes = {
status: () => {
return {
json: (result: any) => {
actualResult = result;
}
};
}
};

afterEach(() => {
sinon.restore();
});

it('should throw a 400 error when no projectId is provided', async () => {
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

try {
const result = delete_submission.deleteOccurrenceSubmission();
await result(
{ ...sampleReq, params: { ...sampleReq.params, projectId: null } },
(null as unknown) as any,
(null as unknown) as any
);
expect.fail();
} catch (actualError) {
expect(actualError.status).to.equal(400);
expect(actualError.message).to.equal('Missing required path param `projectId`');
}
});

it('should throw a 400 error when no surveyId is provided', async () => {
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

try {
const result = delete_submission.deleteOccurrenceSubmission();
await result(
{ ...sampleReq, params: { ...sampleReq.params, surveyId: null } },
(null as unknown) as any,
(null as unknown) as any
);
expect.fail();
} catch (actualError) {
expect(actualError.status).to.equal(400);
expect(actualError.message).to.equal('Missing required path param `surveyId`');
}
});

it('should throw a 400 error when no submissionId is provided', async () => {
sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

try {
const result = delete_submission.deleteOccurrenceSubmission();
await result(
{ ...sampleReq, params: { ...sampleReq.params, submissionId: null } },
(null as unknown) as any,
(null as unknown) as any
);
expect.fail();
} catch (actualError) {
expect(actualError.status).to.equal(400);
expect(actualError.message).to.equal('Missing required path param `submissionId`');
}
});

it('should throw a 400 error when no sql statement returned for deleteOccurrenceSubmissionSQL', async () => {
sinon.stub(db, 'getDBConnection').returns({
...dbConnectionObj,
systemUserId: () => {
return 20;
}
});

sinon.stub(survey_occurrence_queries, 'deleteOccurrenceSubmissionSQL').returns(null);

try {
const result = delete_submission.deleteOccurrenceSubmission();

await result(sampleReq, (null as unknown) as any, (null as unknown) as any);
expect.fail();
} catch (actualError) {
expect(actualError.status).to.equal(400);
expect(actualError.message).to.equal('Failed to build SQL delete statement');
}
});

it('should return null when no rowCount', async () => {
const mockQuery = sinon.stub();

mockQuery.resolves({ rowCount: null });

sinon.stub(db, 'getDBConnection').returns({
...dbConnectionObj,
systemUserId: () => {
return 20;
},
query: mockQuery
});

sinon.stub(survey_occurrence_queries, 'deleteOccurrenceSubmissionSQL').returns(SQL`something`);

const result = delete_submission.deleteOccurrenceSubmission();

await result(sampleReq, sampleRes as any, (null as unknown) as any);

expect(actualResult).to.equal(null);
});

it('should return rowCount on success', async () => {
const mockQuery = sinon.stub();

mockQuery.resolves({ rowCount: 1 });

sinon.stub(db, 'getDBConnection').returns({
...dbConnectionObj,
systemUserId: () => {
return 20;
},
query: mockQuery
});

sinon.stub(survey_occurrence_queries, 'deleteOccurrenceSubmissionSQL').returns(SQL`something`);

const result = delete_submission.deleteOccurrenceSubmission();

await result(sampleReq, sampleRes as any, (null as unknown) as any);

expect(actualResult).to.equal(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SYSTEM_ROLE } from '../../../../../../../../constants/roles';
import { getDBConnection } from '../../../../../../../../database/db';
import { HTTP400 } from '../../../../../../../../errors/CustomError';
import { deleteOccurrenceSubmissionSQL } from '../../../../../../../../queries/survey/survey-occurrence-queries';
import { getLogger } from '../../../../../../../../utils/logger';

const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete');

export const DELETE: Operation = [deleteOccurrenceSubmission()];

DELETE.apiDoc = {
description: 'Soft deletes an occurrence submission by ID.',
tags: ['observation_submission', 'delete'],
security: [
{
Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN]
}
],
parameters: [
{
in: 'path',
name: 'projectId',
schema: {
type: 'number'
},
required: true
},
{
in: 'path',
name: 'surveyId',
schema: {
type: 'number'
},
required: true
},
{
in: 'path',
name: 'submissionId',
schema: {
type: 'number'
},
required: true
}
],
responses: {
200: {
description: 'Observation submission csv details response.',
content: {
'application/json': {
schema: {
title: 'Row count of soft deleted records',
type: 'number'
}
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/401'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

export function deleteOccurrenceSubmission(): RequestHandler {
return async (req, res) => {
defaultLog.debug({
label: 'Soft delete an occurrence submission by ID',
message: 'params',
req_params: req.params
});

if (!req.params.projectId) {
throw new HTTP400('Missing required path param `projectId`');
}

if (!req.params.surveyId) {
throw new HTTP400('Missing required path param `surveyId`');
}

if (!req.params.submissionId) {
throw new HTTP400('Missing required path param `submissionId`');
}

const connection = getDBConnection(req['keycloak_token']);

try {
const deleteSubmissionSQLStatement = deleteOccurrenceSubmissionSQL(Number(req.params.submissionId));

if (!deleteSubmissionSQLStatement) {
throw new HTTP400('Failed to build SQL delete statement');
}

await connection.open();

const deleteResult = await connection.query(
deleteSubmissionSQLStatement.text,
deleteSubmissionSQLStatement.values
);

await connection.commit();

const deleteResponse = (deleteResult && deleteResult.rowCount) || null;

return res.status(200).json(deleteResponse);
} catch (error) {
defaultLog.debug({ label: 'deleteOccurrenceSubmission', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
17 changes: 16 additions & 1 deletion api/src/queries/survey/survey-occurrence-queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
insertSurveyOccurrenceSubmissionSQL,
insertOccurrenceSubmissionMessageSQL,
insertOccurrenceSubmissionStatusSQL,
updateSurveyOccurrenceSubmissionWithKeySQL
updateSurveyOccurrenceSubmissionWithKeySQL,
deleteOccurrenceSubmissionSQL
} from './survey-occurrence-queries';

describe('insertSurveyOccurrenceSubmissionSQL', () => {
Expand Down Expand Up @@ -37,6 +38,20 @@ describe('insertSurveyOccurrenceSubmissionSQL', () => {
});
});

describe('deleteOccurrenceSubmissionSQL', () => {
it('returns null response when null submissionId provided', () => {
const response = deleteOccurrenceSubmissionSQL((null as unknown) as number);

expect(response).to.be.null;
});

it('returns non null response when valid params provided', () => {
const response = deleteOccurrenceSubmissionSQL(1);

expect(response).to.not.be.null;
});
});

describe('getLatestSurveyOccurrenceSubmission', () => {
it('returns null response when null surveyId provided', () => {
const response = getLatestSurveyOccurrenceSubmissionSQL((null as unknown) as number);
Expand Down
Loading