Skip to content

Commit a0fb3dc

Browse files
J-pilonmauberti-bc
andauthored
SIMSBIOHUB 866: Submitting a Data Request (#353)
* feat: adds record_end_date when creating a policy with an expiration date * feat: creates a policy for the team that made the data request * test: data request service * refact: adds private methods to handle responsibilities of creating a data request * feat: adds custom hooks for calling the api to create a data request * feat: adds /request-access client-side route * feat: adds util to determine if any search results are secure * feat: renders banner if any search results are secure * feat: add RequestDataPage with form * cleanup * cleanup * refact: extracts AlertBanner to reusable component * cleanup * refact: uses dialogContext to display errors * misc * refact(minor): updates api docs for findDataRequests endpoint * cleanup * refact: DataRequestService * refact: removes redundant defualt prop * refact: extracts interfaces to their own file * refact: adds DataRequestRouter * refact: renames to 'data request' * fix: tests for createDataRequest * refact: removes util for hasSecuredResults * refact: params for createTeamPolicy * misc --------- Co-authored-by: Macgregor Aubertin-Young <108430771+mauberti-bc@users.noreply.github.com>
1 parent 1bbe9e0 commit a0fb3dc

File tree

19 files changed

+488
-80
lines changed

19 files changed

+488
-80
lines changed

api/src/models/data-request.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const DataRequestFilters = z.object({
1919
export type DataRequestFilters = z.infer<typeof DataRequestFilters>;
2020

2121
export const CreateDataRequest = z.object({
22+
requested_by: z.number().int(),
2223
reason: z.string(),
2324
team_id: z.string().uuid().optional()
2425
});
@@ -29,9 +30,10 @@ export const UpdateDataRequest = z.object({
2930
});
3031
export type UpdateDataRequest = z.infer<typeof UpdateDataRequest>;
3132

32-
// ──────────────────────────────────────────────────────────────────────────────
33-
// data_request CRUD Responses
34-
// ──────────────────────────────────────────────────────────────────────────────
33+
export interface CreateTeamPolicyParams {
34+
teamId: string;
35+
policyId: string;
36+
}
3537

3638
export const FlatDataRequestWithStatus = DataRequest.extend(DataRequestStatus.shape);
3739
export type FlatDataRequestWithStatus = z.infer<typeof FlatDataRequestWithStatus>;

api/src/models/policy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type Policy = z.infer<typeof Policy>;
1313
export interface CreatePolicy {
1414
name: string;
1515
description?: string;
16+
record_end_date?: string;
1617
}
1718

1819
export interface UpdatePolicy {

api/src/paths/data-request/index.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ describe('data-request', () => {
221221

222222
await requestHandler(mockReq, mockRes, mockNext);
223223

224-
expect(stub).to.have.been.calledOnceWith(mockDBConnection.systemUserId(), {
224+
expect(stub).to.have.been.calledOnceWith({
225+
requested_by: mockDBConnection.systemUserId(),
225226
reason: 'Research purposes',
226227
team_id: mockDataRequestWithStatus.team_id
227228
});
@@ -248,7 +249,8 @@ describe('data-request', () => {
248249

249250
await requestHandler(mockReq, mockRes, mockNext);
250251

251-
expect(stub).to.have.been.calledOnceWith(mockDBConnection.systemUserId(), {
252+
expect(stub).to.have.been.calledOnceWith({
253+
requested_by: mockDBConnection.systemUserId(),
252254
reason: 'Research purposes',
253255
team_id: undefined
254256
});

api/src/paths/data-request/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export const POST: Operation = [
4040
];
4141

4242
GET.apiDoc = {
43-
description: 'Find all data request records, optionally filtered by date range, requested_by, team_id, or status',
43+
description:
44+
'Find data request records for teams the current user belongs to, optionally filtered by date range, requested_by, team_id, or status',
4445
tags: ['data-request'],
4546
security: [
4647
{
@@ -125,7 +126,7 @@ POST.apiDoc = {
125126
};
126127

127128
/**
128-
* Find all data request records, optionally filtered by query params.
129+
* Find data request records for teams the current user belongs to, optionally filtered by query params.
129130
*
130131
* @returns {RequestHandler}
131132
*/
@@ -172,7 +173,11 @@ export function createDataRequest(): RequestHandler {
172173

173174
const dataRequestService = new DataRequestService(connection);
174175

175-
const dataRequest = await dataRequestService.createDataRequest(systemUserId, { reason, team_id: teamId });
176+
const dataRequest = await dataRequestService.createDataRequest({
177+
requested_by: systemUserId,
178+
reason,
179+
team_id: teamId
180+
});
176181

177182
await connection.commit();
178183

api/src/repositories/authorization/policy-repository.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export class PolicyRepository extends BaseRepository {
3232
.table('policy')
3333
.insert({
3434
name: policyData.name,
35-
description: policyData.description
35+
description: policyData.description,
36+
record_end_date: policyData.record_end_date
3637
})
3738
.returning(['policy_id', 'name', 'description']);
3839

@@ -147,7 +148,7 @@ export class PolicyRepository extends BaseRepository {
147148
INNER JOIN policy_urn_parts ps
148149
ON ps.policy_id = p.policy_id
149150
WHERE tm.system_user_id = ${systemUserId}
150-
AND p.record_end_date IS NULL
151+
AND (p.record_end_date IS NULL OR p.record_end_date > NOW())
151152
AND (ps.part1 = ${urnParts.submissionId} OR ps.part1 = '*')
152153
AND (ps.part2 = ${urnParts.featureTypeName} OR ps.part2 = '*')
153154
AND (ps.part3 = ${urnParts.submissionFeatureId} OR ps.part3 = '*')

api/src/repositories/data-request-repository.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ describe('DataRequestRepository', () => {
239239

240240
const repo = new DataRequestRepository(mockDBConnection);
241241

242-
const payload: CreateDataRequest = { reason: 'New research project' };
242+
const payload: CreateDataRequest = {
243+
requested_by: mockDataRequest.requested_by,
244+
reason: 'New research project'
245+
};
243246

244247
const result = await repo.createDataRequest(mockDataRequest.requested_by, payload);
245248

@@ -258,7 +261,10 @@ describe('DataRequestRepository', () => {
258261

259262
const repo = new DataRequestRepository(mockDBConnection);
260263

261-
const payload: CreateDataRequest = { reason: 'Test' };
264+
const payload: CreateDataRequest = {
265+
requested_by: mockDataRequest.requested_by,
266+
reason: 'Test'
267+
};
262268

263269
try {
264270
await repo.createDataRequest(mockDataRequest.requested_by, payload);

api/src/services/data-request-service.test.ts

Lines changed: 96 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import sinon from 'sinon';
44
import sinonChai from 'sinon-chai';
55
import { CreateDataRequest, DataRequest, FlatDataRequestWithStatus, UpdateDataRequest } from '../models/data-request';
66
import { DataRequestStatus, DataRequestStatusEnum } from '../models/data-request-status';
7+
import { PolicyEffect } from '../models/policy-statement';
78
import { TeamMemberWithUser } from '../models/team-member';
89
import { DataRequestRepository } from '../repositories/data-request-repository';
910
import { getMockDBConnection } from '../__mocks__/db';
11+
import { PolicyService } from './access-policy/policy-service';
1012
import { TeamMemberService } from './access-policy/team-member-service';
13+
import { TeamPolicyService } from './access-policy/team-policy-service';
1114
import { TeamService } from './access-policy/team-service';
1215
import { DataRequestService } from './data-request-service';
1316
import { DataRequestStatusService } from './data-request-status-service';
@@ -197,62 +200,132 @@ describe('DataRequestService', () => {
197200
});
198201

199202
describe('createDataRequest', () => {
200-
it('should create a data request with provided team_id and return it with status', async () => {
203+
const mockApprovedStatus: DataRequestStatus = {
204+
...mockDataRequestStatus,
205+
request_status: 'APPROVED'
206+
};
207+
208+
const mockPolicy = {
209+
policy_id: 'f6a7b8c9-d0e1-2345-fabc-456789012345',
210+
name: 'Data request policy - uuid',
211+
description: null,
212+
statements: []
213+
};
214+
215+
const mockTeamPolicy = {
216+
team_policy_id: '12345678-abcd-ef01-2345-678901234567',
217+
team_id: mockDataRequest.team_id,
218+
policy_id: mockPolicy.policy_id
219+
};
220+
221+
const stubCreateDataRequestDependencies = (
222+
overrides: {
223+
teamId?: string;
224+
createNewTeam?: boolean;
225+
} = {}
226+
) => {
227+
const { teamId = mockDataRequest.team_id, createNewTeam = false } = overrides;
228+
229+
if (createNewTeam) {
230+
sinon
231+
.stub(TeamService.prototype, 'createTeam')
232+
.resolves({ team_id: teamId, name: 'test', description: null, member_count: 1 });
233+
sinon.stub(TeamMemberService.prototype, 'createTeamMember').resolves(mockTeamMember);
234+
}
235+
236+
sinon
237+
.stub(DataRequestRepository.prototype, 'createDataRequest')
238+
.resolves({ ...mockDataRequest, team_id: teamId });
239+
sinon.stub(PolicyService.prototype, 'createPolicyWithStatements').resolves(mockPolicy);
240+
sinon.stub(TeamPolicyService.prototype, 'createTeamPolicy').resolves({ ...mockTeamPolicy, team_id: teamId });
241+
sinon.stub(DataRequestStatusService.prototype, 'createDataRequestStatus').resolves(mockApprovedStatus);
242+
};
243+
244+
it('should create a data request with provided team_id, create a policy, and auto-approve', async () => {
201245
const mockDB = getMockDBConnection();
202246
const service = new DataRequestService(mockDB);
203247

204-
const payload: CreateDataRequest = { reason: 'New research project', team_id: mockDataRequest.team_id };
205-
const createStub = sinon.stub(DataRequestRepository.prototype, 'createDataRequest').resolves(mockDataRequest);
206-
const statusStub = sinon
207-
.stub(DataRequestStatusService.prototype, 'createDataRequestStatus')
208-
.resolves(mockDataRequestStatus);
248+
const payload: CreateDataRequest = {
249+
requested_by: mockDataRequest.requested_by,
250+
reason: 'New research project',
251+
team_id: mockDataRequest.team_id
252+
};
253+
254+
stubCreateDataRequestDependencies();
255+
256+
const createStub = DataRequestRepository.prototype.createDataRequest as sinon.SinonStub;
257+
const policyStub = PolicyService.prototype.createPolicyWithStatements as sinon.SinonStub;
258+
const teamPolicyStub = TeamPolicyService.prototype.createTeamPolicy as sinon.SinonStub;
259+
const statusStub = DataRequestStatusService.prototype.createDataRequestStatus as sinon.SinonStub;
260+
261+
const result = await service.createDataRequest(payload);
262+
263+
expect(createStub).to.have.been.calledOnceWith(mockDataRequest.requested_by, {
264+
...payload,
265+
team_id: mockDataRequest.team_id
266+
});
267+
268+
expect(policyStub).to.have.been.calledOnce;
269+
const policyArgs = policyStub.firstCall.args;
270+
expect(policyArgs[1]).to.deep.equal([{ effect: PolicyEffect.ALLOW, submission_feature_urn: 'urn:*:*:*' }]);
271+
expect(policyArgs[0]).to.have.property('record_end_date').that.is.a('string');
209272

210-
const result = await service.createDataRequest(mockDataRequest.requested_by, payload);
273+
expect(teamPolicyStub).to.have.been.calledOnceWith({
274+
team_id: mockDataRequest.team_id,
275+
policy_id: mockPolicy.policy_id
276+
});
211277

212-
expect(createStub).to.have.been.calledOnceWith(mockDataRequest.requested_by, payload);
213278
expect(statusStub).to.have.been.calledOnceWith(
214279
mockDataRequest.data_request_id,
215-
DataRequestStatusEnum.enum.REQUESTED,
280+
DataRequestStatusEnum.enum.APPROVED,
216281
undefined
217282
);
283+
218284
expect(result.data_request_id).to.equal(mockDataRequest.data_request_id);
219-
expect(result.data_request_status).to.deep.equal(mockDataRequestStatus);
285+
expect(result.data_request_status.request_status).to.equal('APPROVED');
220286
});
221287

222288
it('should create a new team when payload.team_id is undefined', async () => {
223289
const mockDB = getMockDBConnection();
224290
const service = new DataRequestService(mockDB);
225291

226292
const newTeamId = 'e5f6a7b8-c9d0-1234-efab-345678901234';
227-
const teamStub = sinon
228-
.stub(TeamService.prototype, 'createTeam')
229-
.resolves({ team_id: newTeamId, name: 'test', description: null, member_count: 0 });
230-
const memberStub = sinon.stub(TeamMemberService.prototype, 'createTeamMember').resolves(mockTeamMember);
231-
sinon.stub(DataRequestRepository.prototype, 'createDataRequest').resolves({
232-
...mockDataRequest,
233-
team_id: newTeamId
234-
});
235-
sinon.stub(DataRequestStatusService.prototype, 'createDataRequestStatus').resolves(mockDataRequestStatus);
293+
stubCreateDataRequestDependencies({ teamId: newTeamId, createNewTeam: true });
236294

237-
const payload: CreateDataRequest = { reason: 'New research project' };
238-
await service.createDataRequest(mockDataRequest.requested_by, payload);
295+
const teamStub = TeamService.prototype.createTeam as sinon.SinonStub;
296+
const memberStub = TeamMemberService.prototype.createTeamMember as sinon.SinonStub;
297+
const teamPolicyStub = TeamPolicyService.prototype.createTeamPolicy as sinon.SinonStub;
298+
299+
const payload: CreateDataRequest = {
300+
requested_by: mockDataRequest.requested_by,
301+
reason: 'New research project'
302+
};
303+
await service.createDataRequest(payload);
239304

240305
expect(teamStub).to.have.been.calledOnce;
241306
expect(memberStub).to.have.been.calledOnceWith({
242307
system_user_id: mockDataRequest.requested_by,
243308
team_id: newTeamId
244309
});
310+
expect(teamPolicyStub).to.have.been.calledOnceWith({
311+
team_id: newTeamId,
312+
policy_id: mockPolicy.policy_id
313+
});
245314
});
246315

247316
it('should propagate repository errors', async () => {
248317
const mockDB = getMockDBConnection();
249318
const service = new DataRequestService(mockDB);
250319

251-
const payload: CreateDataRequest = { reason: 'Test', team_id: mockDataRequest.team_id };
320+
const payload: CreateDataRequest = {
321+
requested_by: 1,
322+
reason: 'Test',
323+
team_id: mockDataRequest.team_id
324+
};
252325
sinon.stub(DataRequestRepository.prototype, 'createDataRequest').rejects(new Error('DB error'));
253326

254327
try {
255-
await service.createDataRequest(1, payload);
328+
await service.createDataRequest(payload);
256329
throw new Error('Expected to throw');
257330
} catch (err) {
258331
expect(err).to.be.instanceOf(Error);

0 commit comments

Comments
 (0)