1use thiserror::Error;
8
9pub use ahri_tre_core::{
10 ApproveCustodianStudyAccessRequest, CustodianStudyAccessRequest,
11 CustodianStudyAccessRequestApprovalOutcome, CustodianStudyAccessRequestApprovalPreflight,
12 CustodianStudyAccessRequestDecision, CustodianStudyAccessRequestPort,
13 CustodianStudyAccessRequestPortError, CustodianStudyAccessRequestQueuePort,
14 CustodianStudyAccessRequestRejectionOutcome, CustodianStudyAccessRequestStatusFilter,
15 CustodianStudyAccessRequests, ProvisionStudyAccessRequestWorkflowEntry,
16 ReadyAccessRequestDatastore, ReadyAccessRequestDatastoreResolver,
17 RecordCustodianStudyAccessRequestApproval, RejectCustodianStudyAccessRequest,
18 RequesterStudyAccessRequest, RequesterStudyAccessRequestPort,
19 RequesterStudyAccessRequestPortError, RequesterStudyAccessRequestWithdrawalOutcome,
20 RequesterStudyAccessRequests, StudyAccessRequestDecision, StudyAccessRequestLifecycle,
21 StudyAccessRequestSubmissionOutcome, StudyAccessRequestSubmissionPort,
22 StudyAccessRequestSubmissionPortError, StudyAccessRequestWorkflowEntryPort,
23 StudyAccessRequestWorkflowEntryPortError, SubmitStudyAccessRequest,
24 SubmittedStudyAccessRequest,
25};
26
27#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
29pub enum SubmitStudyAccessRequestError {
30 #[error("selected datastore identity is invalid")]
31 InvalidDatastoreIdentity,
32 #[error("access-request service authority is unavailable")]
33 AuthorityUnavailable,
34 #[error("selected datastore is unavailable")]
35 DatastoreUnavailable,
36 #[error("study was not found in the selected datastore")]
37 StudyNotFound,
38 #[error("intended use is required")]
39 IntendedUseRequired,
40 #[error("a pending request already exists for this study")]
41 DuplicatePendingRequest,
42}
43
44#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
46pub enum RequesterStudyAccessRequestError {
47 #[error("selected datastore identity is invalid")]
48 InvalidDatastoreIdentity,
49 #[error("access-request service authority is unavailable")]
50 AuthorityUnavailable,
51 #[error("selected datastore is unavailable")]
52 DatastoreUnavailable,
53 #[error("request was not found for the current visitor")]
54 RequestNotFound,
55 #[error("request is not pending and cannot be withdrawn")]
56 RequestNotPending,
57 #[error("request has an invalid lifecycle state")]
58 InvalidLifecycleState,
59}
60
61#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
63pub enum CustodianStudyAccessRequestError {
64 #[error("selected datastore identity is invalid")]
65 InvalidDatastoreIdentity,
66 #[error("access-request authority is unavailable")]
67 AuthorityUnavailable,
68 #[error("an authenticated TRE user session is required")]
69 AuthenticatedTreUserRequired,
70 #[error("selected datastore is unavailable")]
71 DatastoreUnavailable,
72 #[error("request was not found")]
73 RequestNotFound,
74 #[error("the authenticated TRE user is not a custodian for this study")]
75 CustodianshipRequired,
76 #[error("request is not pending")]
77 RequestNotPending,
78 #[error("request has an invalid lifecycle state")]
79 InvalidLifecycleState,
80}
81
82pub fn submit_study_access_request(
85 port: &mut dyn StudyAccessRequestSubmissionPort,
86 mut request: SubmitStudyAccessRequest,
87) -> Result<SubmittedStudyAccessRequest, SubmitStudyAccessRequestError> {
88 request.intended_use = required_text(&request.intended_use)
89 .ok_or(SubmitStudyAccessRequestError::IntendedUseRequired)?;
90 request.contact = optional_text(request.contact.as_deref());
91 request.affiliation = optional_text(request.affiliation.as_deref());
92
93 match port
94 .submit_study_access_request(&request)
95 .map_err(map_port_error)?
96 {
97 StudyAccessRequestSubmissionOutcome::Submitted(submission) => Ok(submission),
98 StudyAccessRequestSubmissionOutcome::StudyNotFound => {
99 Err(SubmitStudyAccessRequestError::StudyNotFound)
100 }
101 StudyAccessRequestSubmissionOutcome::DuplicatePendingRequest => {
102 Err(SubmitStudyAccessRequestError::DuplicatePendingRequest)
103 }
104 }
105}
106
107fn map_port_error(error: StudyAccessRequestSubmissionPortError) -> SubmitStudyAccessRequestError {
108 match error {
109 StudyAccessRequestSubmissionPortError::InvalidDatastoreIdentity => {
110 SubmitStudyAccessRequestError::InvalidDatastoreIdentity
111 }
112 StudyAccessRequestSubmissionPortError::AuthorityUnavailable => {
113 SubmitStudyAccessRequestError::AuthorityUnavailable
114 }
115 StudyAccessRequestSubmissionPortError::DatastoreUnavailable => {
116 SubmitStudyAccessRequestError::DatastoreUnavailable
117 }
118 }
119}
120
121pub fn list_requester_study_access_requests(
124 port: &mut dyn RequesterStudyAccessRequestPort,
125 scope: &RequesterStudyAccessRequests,
126) -> Result<Vec<RequesterStudyAccessRequest>, RequesterStudyAccessRequestError> {
127 port.list_requester_study_access_requests(scope)
128 .map_err(map_requester_port_error)
129}
130
131pub fn get_requester_study_access_request(
134 port: &mut dyn RequesterStudyAccessRequestPort,
135 scope: &RequesterStudyAccessRequests,
136 request_id: uuid::Uuid,
137) -> Result<RequesterStudyAccessRequest, RequesterStudyAccessRequestError> {
138 port.get_requester_study_access_request(scope, request_id)
139 .map_err(map_requester_port_error)?
140 .ok_or(RequesterStudyAccessRequestError::RequestNotFound)
141}
142
143pub fn withdraw_requester_study_access_request(
147 port: &mut dyn RequesterStudyAccessRequestPort,
148 scope: &RequesterStudyAccessRequests,
149 request_id: uuid::Uuid,
150) -> Result<RequesterStudyAccessRequest, RequesterStudyAccessRequestError> {
151 match port
152 .withdraw_requester_study_access_request(scope, request_id)
153 .map_err(map_requester_port_error)?
154 {
155 RequesterStudyAccessRequestWithdrawalOutcome::Withdrawn(request) => Ok(request),
156 RequesterStudyAccessRequestWithdrawalOutcome::NotFound => {
157 Err(RequesterStudyAccessRequestError::RequestNotFound)
158 }
159 RequesterStudyAccessRequestWithdrawalOutcome::NotPending => {
160 Err(RequesterStudyAccessRequestError::RequestNotPending)
161 }
162 }
163}
164
165fn map_requester_port_error(
166 error: RequesterStudyAccessRequestPortError,
167) -> RequesterStudyAccessRequestError {
168 match error {
169 RequesterStudyAccessRequestPortError::InvalidDatastoreIdentity => {
170 RequesterStudyAccessRequestError::InvalidDatastoreIdentity
171 }
172 RequesterStudyAccessRequestPortError::AuthorityUnavailable => {
173 RequesterStudyAccessRequestError::AuthorityUnavailable
174 }
175 RequesterStudyAccessRequestPortError::DatastoreUnavailable => {
176 RequesterStudyAccessRequestError::DatastoreUnavailable
177 }
178 RequesterStudyAccessRequestPortError::InvalidLifecycleState => {
179 RequesterStudyAccessRequestError::InvalidLifecycleState
180 }
181 }
182}
183
184pub fn list_custodian_study_access_requests(
187 port: &mut dyn CustodianStudyAccessRequestQueuePort,
188 scope: &CustodianStudyAccessRequests,
189) -> Result<Vec<CustodianStudyAccessRequest>, CustodianStudyAccessRequestError> {
190 port.list_custodian_study_access_requests(scope)
191 .map_err(map_custodian_port_error)
192}
193
194pub fn reject_custodian_study_access_request(
197 port: &mut dyn CustodianStudyAccessRequestPort,
198 mut rejection: RejectCustodianStudyAccessRequest,
199) -> Result<CustodianStudyAccessRequest, CustodianStudyAccessRequestError> {
200 rejection.reason = optional_text(rejection.reason.as_deref());
201 match port
202 .reject_custodian_study_access_request(&rejection)
203 .map_err(map_custodian_port_error)?
204 {
205 CustodianStudyAccessRequestRejectionOutcome::Rejected(request) => Ok(*request),
206 CustodianStudyAccessRequestRejectionOutcome::NotFound => {
207 Err(CustodianStudyAccessRequestError::RequestNotFound)
208 }
209 CustodianStudyAccessRequestRejectionOutcome::NotCustodian => {
210 Err(CustodianStudyAccessRequestError::CustodianshipRequired)
211 }
212 CustodianStudyAccessRequestRejectionOutcome::NotPending => {
213 Err(CustodianStudyAccessRequestError::RequestNotPending)
214 }
215 }
216}
217
218pub fn approve_custodian_study_access_request(
225 custodian_port: &mut dyn CustodianStudyAccessRequestPort,
226 workflow_entry_port: &mut dyn StudyAccessRequestWorkflowEntryPort,
227 approval: ApproveCustodianStudyAccessRequest,
228) -> Result<CustodianStudyAccessRequest, CustodianStudyAccessRequestError> {
229 let requester_orcid = match custodian_port
230 .preflight_custodian_study_access_request_approval(&approval)
231 .map_err(map_custodian_port_error)?
232 {
233 CustodianStudyAccessRequestApprovalPreflight::Pending { requester_orcid } => {
234 requester_orcid
235 }
236 CustodianStudyAccessRequestApprovalPreflight::NotFound => {
237 return Err(CustodianStudyAccessRequestError::RequestNotFound);
238 }
239 CustodianStudyAccessRequestApprovalPreflight::NotCustodian => {
240 return Err(CustodianStudyAccessRequestError::CustodianshipRequired);
241 }
242 CustodianStudyAccessRequestApprovalPreflight::NotPending => {
243 return Err(CustodianStudyAccessRequestError::RequestNotPending);
244 }
245 };
246 let requester_role = workflow_entry_port
247 .provision_study_access_request_workflow_entry(&ProvisionStudyAccessRequestWorkflowEntry {
248 datastore_id: approval.datastore_id,
249 requester_orcid,
250 })
251 .map_err(map_workflow_entry_port_error)?;
252 match custodian_port
253 .record_custodian_study_access_request_approval(
254 &RecordCustodianStudyAccessRequestApproval {
255 datastore_id: approval.datastore_id,
256 request_id: approval.request_id,
257 requester_role,
258 },
259 )
260 .map_err(map_custodian_port_error)?
261 {
262 CustodianStudyAccessRequestApprovalOutcome::Approved(request) => Ok(*request),
263 CustodianStudyAccessRequestApprovalOutcome::NotPending => {
264 Err(CustodianStudyAccessRequestError::RequestNotPending)
265 }
266 }
267}
268
269fn map_custodian_port_error(
270 error: CustodianStudyAccessRequestPortError,
271) -> CustodianStudyAccessRequestError {
272 match error {
273 CustodianStudyAccessRequestPortError::InvalidDatastoreIdentity => {
274 CustodianStudyAccessRequestError::InvalidDatastoreIdentity
275 }
276 CustodianStudyAccessRequestPortError::AuthorityUnavailable => {
277 CustodianStudyAccessRequestError::AuthorityUnavailable
278 }
279 CustodianStudyAccessRequestPortError::AuthenticatedTreUserRequired => {
280 CustodianStudyAccessRequestError::AuthenticatedTreUserRequired
281 }
282 CustodianStudyAccessRequestPortError::CustodianshipRequired => {
283 CustodianStudyAccessRequestError::CustodianshipRequired
284 }
285 CustodianStudyAccessRequestPortError::DatastoreUnavailable => {
286 CustodianStudyAccessRequestError::DatastoreUnavailable
287 }
288 CustodianStudyAccessRequestPortError::InvalidLifecycleState => {
289 CustodianStudyAccessRequestError::InvalidLifecycleState
290 }
291 }
292}
293
294fn map_workflow_entry_port_error(
295 error: StudyAccessRequestWorkflowEntryPortError,
296) -> CustodianStudyAccessRequestError {
297 match error {
298 StudyAccessRequestWorkflowEntryPortError::AuthorityUnavailable => {
299 CustodianStudyAccessRequestError::AuthorityUnavailable
300 }
301 StudyAccessRequestWorkflowEntryPortError::DatastoreUnavailable => {
302 CustodianStudyAccessRequestError::DatastoreUnavailable
303 }
304 }
305}
306
307fn required_text(value: &str) -> Option<String> {
308 let value = value.trim();
309 (!value.is_empty()).then(|| value.to_string())
310}
311
312fn optional_text(value: Option<&str>) -> Option<String> {
313 value.and_then(required_text)
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use ahri_tre_types::StudyId;
320 use chrono::Utc;
321 use std::collections::VecDeque;
322 use uuid::Uuid;
323
324 #[derive(Debug)]
325 struct FakeSubmissionPort {
326 result: Result<StudyAccessRequestSubmissionOutcome, StudyAccessRequestSubmissionPortError>,
327 received: Vec<SubmitStudyAccessRequest>,
328 }
329
330 impl FakeSubmissionPort {
331 fn returning(
332 result: Result<
333 StudyAccessRequestSubmissionOutcome,
334 StudyAccessRequestSubmissionPortError,
335 >,
336 ) -> Self {
337 Self {
338 result,
339 received: Vec::new(),
340 }
341 }
342 }
343
344 impl StudyAccessRequestSubmissionPort for FakeSubmissionPort {
345 fn submit_study_access_request(
346 &mut self,
347 request: &SubmitStudyAccessRequest,
348 ) -> Result<StudyAccessRequestSubmissionOutcome, StudyAccessRequestSubmissionPortError>
349 {
350 self.received.push(request.clone());
351 self.result.clone()
352 }
353 }
354
355 #[derive(Debug)]
356 struct FakeRequesterPort {
357 listed: Result<Vec<RequesterStudyAccessRequest>, RequesterStudyAccessRequestPortError>,
358 fetched: Result<Option<RequesterStudyAccessRequest>, RequesterStudyAccessRequestPortError>,
359 withdrawn: Result<
360 RequesterStudyAccessRequestWithdrawalOutcome,
361 RequesterStudyAccessRequestPortError,
362 >,
363 received_scopes: Vec<RequesterStudyAccessRequests>,
364 received_request_ids: Vec<Uuid>,
365 }
366
367 impl RequesterStudyAccessRequestPort for FakeRequesterPort {
368 fn list_requester_study_access_requests(
369 &mut self,
370 scope: &RequesterStudyAccessRequests,
371 ) -> Result<Vec<RequesterStudyAccessRequest>, RequesterStudyAccessRequestPortError>
372 {
373 self.received_scopes.push(scope.clone());
374 self.listed.clone()
375 }
376
377 fn get_requester_study_access_request(
378 &mut self,
379 scope: &RequesterStudyAccessRequests,
380 request_id: Uuid,
381 ) -> Result<Option<RequesterStudyAccessRequest>, RequesterStudyAccessRequestPortError>
382 {
383 self.received_scopes.push(scope.clone());
384 self.received_request_ids.push(request_id);
385 self.fetched.clone()
386 }
387
388 fn withdraw_requester_study_access_request(
389 &mut self,
390 scope: &RequesterStudyAccessRequests,
391 request_id: Uuid,
392 ) -> Result<
393 RequesterStudyAccessRequestWithdrawalOutcome,
394 RequesterStudyAccessRequestPortError,
395 > {
396 self.received_scopes.push(scope.clone());
397 self.received_request_ids.push(request_id);
398 self.withdrawn.clone()
399 }
400 }
401
402 #[derive(Debug)]
403 struct FakeCustodianPort {
404 listed: Result<Vec<CustodianStudyAccessRequest>, CustodianStudyAccessRequestPortError>,
405 rejected: Result<
406 CustodianStudyAccessRequestRejectionOutcome,
407 CustodianStudyAccessRequestPortError,
408 >,
409 preflight: VecDeque<
410 Result<
411 CustodianStudyAccessRequestApprovalPreflight,
412 CustodianStudyAccessRequestPortError,
413 >,
414 >,
415 recorded: VecDeque<
416 Result<
417 CustodianStudyAccessRequestApprovalOutcome,
418 CustodianStudyAccessRequestPortError,
419 >,
420 >,
421 received_scopes: Vec<CustodianStudyAccessRequests>,
422 received_rejections: Vec<RejectCustodianStudyAccessRequest>,
423 received_approval_preflights: Vec<ApproveCustodianStudyAccessRequest>,
424 received_recorded_approvals: Vec<RecordCustodianStudyAccessRequestApproval>,
425 }
426
427 impl CustodianStudyAccessRequestQueuePort for FakeCustodianPort {
428 fn list_custodian_study_access_requests(
429 &mut self,
430 scope: &CustodianStudyAccessRequests,
431 ) -> Result<Vec<CustodianStudyAccessRequest>, CustodianStudyAccessRequestPortError>
432 {
433 self.received_scopes.push(*scope);
434 self.listed.clone()
435 }
436 }
437
438 impl CustodianStudyAccessRequestPort for FakeCustodianPort {
439 fn reject_custodian_study_access_request(
440 &mut self,
441 rejection: &RejectCustodianStudyAccessRequest,
442 ) -> Result<CustodianStudyAccessRequestRejectionOutcome, CustodianStudyAccessRequestPortError>
443 {
444 self.received_rejections.push(rejection.clone());
445 self.rejected.clone()
446 }
447
448 fn preflight_custodian_study_access_request_approval(
449 &mut self,
450 approval: &ApproveCustodianStudyAccessRequest,
451 ) -> Result<
452 CustodianStudyAccessRequestApprovalPreflight,
453 CustodianStudyAccessRequestPortError,
454 > {
455 self.received_approval_preflights.push(*approval);
456 self.preflight
457 .pop_front()
458 .expect("fake preflight result is configured")
459 }
460
461 fn record_custodian_study_access_request_approval(
462 &mut self,
463 approval: &RecordCustodianStudyAccessRequestApproval,
464 ) -> Result<CustodianStudyAccessRequestApprovalOutcome, CustodianStudyAccessRequestPortError>
465 {
466 self.received_recorded_approvals.push(approval.clone());
467 self.recorded
468 .pop_front()
469 .expect("fake governed approval result is configured")
470 }
471 }
472
473 #[derive(Debug)]
474 struct FakeWorkflowEntryPort {
475 results: VecDeque<Result<String, StudyAccessRequestWorkflowEntryPortError>>,
476 received: Vec<ProvisionStudyAccessRequestWorkflowEntry>,
477 }
478
479 impl StudyAccessRequestWorkflowEntryPort for FakeWorkflowEntryPort {
480 fn provision_study_access_request_workflow_entry(
481 &mut self,
482 provisioning: &ProvisionStudyAccessRequestWorkflowEntry,
483 ) -> Result<String, StudyAccessRequestWorkflowEntryPortError> {
484 self.received.push(provisioning.clone());
485 self.results
486 .pop_front()
487 .expect("fake workflow-entry result is configured")
488 }
489 }
490
491 fn requester_scope() -> RequesterStudyAccessRequests {
492 RequesterStudyAccessRequests {
493 datastore_id: Uuid::from_u128(1),
494 requester_orcid: "0000-0001-2345-6789".to_string(),
495 }
496 }
497
498 fn requester_record(lifecycle: StudyAccessRequestLifecycle) -> RequesterStudyAccessRequest {
499 RequesterStudyAccessRequest {
500 request_id: Uuid::from_u128(3),
501 datastore_id: Uuid::from_u128(1),
502 study_id: StudyId(Uuid::from_u128(2)),
503 intended_use: "research".to_string(),
504 contact: Some("researcher@example.test".to_string()),
505 affiliation: None,
506 lifecycle,
507 submitted_at: Utc::now(),
508 decision: StudyAccessRequestDecision {
509 decided_at: None,
510 reason: None,
511 },
512 }
513 }
514
515 fn custodian_scope() -> CustodianStudyAccessRequests {
516 CustodianStudyAccessRequests {
517 datastore_id: Uuid::from_u128(1),
518 status: CustodianStudyAccessRequestStatusFilter::Rejected,
519 }
520 }
521
522 fn custodian_record(lifecycle: StudyAccessRequestLifecycle) -> CustodianStudyAccessRequest {
523 CustodianStudyAccessRequest {
524 request_id: Uuid::from_u128(3),
525 datastore_id: Uuid::from_u128(1),
526 study_id: StudyId(Uuid::from_u128(2)),
527 requester_orcid: "0000-0001-2345-6789".to_string(),
528 intended_use: "research".to_string(),
529 contact: Some("researcher@example.test".to_string()),
530 affiliation: None,
531 lifecycle,
532 submitted_at: Utc::now(),
533 decision: CustodianStudyAccessRequestDecision {
534 decided_at: None,
535 decided_by: None,
536 reason: None,
537 },
538 }
539 }
540
541 fn custodian_port(
542 listed: Result<Vec<CustodianStudyAccessRequest>, CustodianStudyAccessRequestPortError>,
543 rejected: Result<
544 CustodianStudyAccessRequestRejectionOutcome,
545 CustodianStudyAccessRequestPortError,
546 >,
547 ) -> FakeCustodianPort {
548 FakeCustodianPort {
549 listed,
550 rejected,
551 preflight: VecDeque::from([Ok(
552 CustodianStudyAccessRequestApprovalPreflight::NotPending,
553 )]),
554 recorded: VecDeque::from([Ok(CustodianStudyAccessRequestApprovalOutcome::NotPending)]),
555 received_scopes: Vec::new(),
556 received_rejections: Vec::new(),
557 received_approval_preflights: Vec::new(),
558 received_recorded_approvals: Vec::new(),
559 }
560 }
561
562 fn requester_port(
563 listed: Result<Vec<RequesterStudyAccessRequest>, RequesterStudyAccessRequestPortError>,
564 fetched: Result<Option<RequesterStudyAccessRequest>, RequesterStudyAccessRequestPortError>,
565 withdrawn: Result<
566 RequesterStudyAccessRequestWithdrawalOutcome,
567 RequesterStudyAccessRequestPortError,
568 >,
569 ) -> FakeRequesterPort {
570 FakeRequesterPort {
571 listed,
572 fetched,
573 withdrawn,
574 received_scopes: Vec::new(),
575 received_request_ids: Vec::new(),
576 }
577 }
578
579 fn request() -> SubmitStudyAccessRequest {
580 SubmitStudyAccessRequest {
581 datastore_id: Uuid::from_u128(1),
582 study_id: StudyId(Uuid::from_u128(2)),
583 requester_orcid: "0000-0001-2345-6789".to_string(),
584 requester_subject: "orcid-subject".to_string(),
585 intended_use: " controlled research ".to_string(),
586 contact: Some(" researcher@example.test ".to_string()),
587 affiliation: Some(" ".to_string()),
588 }
589 }
590
591 #[test]
592 fn submission_passes_normalized_request_to_port_and_returns_submission() {
593 let submitted_at = Utc::now();
594 let mut port = FakeSubmissionPort::returning(Ok(
595 StudyAccessRequestSubmissionOutcome::Submitted(SubmittedStudyAccessRequest {
596 request_id: Uuid::from_u128(3),
597 submitted_at,
598 }),
599 ));
600
601 let result = submit_study_access_request(&mut port, request()).unwrap();
602
603 assert_eq!(result.request_id, Uuid::from_u128(3));
604 assert_eq!(result.submitted_at, submitted_at);
605 assert_eq!(port.received.len(), 1);
606 assert_eq!(port.received[0].intended_use, "controlled research");
607 assert_eq!(
608 port.received[0].contact.as_deref(),
609 Some("researcher@example.test")
610 );
611 assert_eq!(port.received[0].affiliation, None);
612 }
613
614 #[test]
615 fn submission_rejects_missing_intended_use_before_calling_port() {
616 let mut port =
617 FakeSubmissionPort::returning(Ok(StudyAccessRequestSubmissionOutcome::StudyNotFound));
618 let mut submission = request();
619 submission.intended_use = " \t ".to_string();
620
621 let error = submit_study_access_request(&mut port, submission).unwrap_err();
622
623 assert_eq!(error, SubmitStudyAccessRequestError::IntendedUseRequired);
624 assert!(port.received.is_empty());
625 }
626
627 #[test]
628 fn submission_preserves_semantic_and_infrastructure_failures() {
629 let cases = [
630 (
631 Ok(StudyAccessRequestSubmissionOutcome::StudyNotFound),
632 SubmitStudyAccessRequestError::StudyNotFound,
633 ),
634 (
635 Ok(StudyAccessRequestSubmissionOutcome::DuplicatePendingRequest),
636 SubmitStudyAccessRequestError::DuplicatePendingRequest,
637 ),
638 (
639 Err(StudyAccessRequestSubmissionPortError::InvalidDatastoreIdentity),
640 SubmitStudyAccessRequestError::InvalidDatastoreIdentity,
641 ),
642 (
643 Err(StudyAccessRequestSubmissionPortError::AuthorityUnavailable),
644 SubmitStudyAccessRequestError::AuthorityUnavailable,
645 ),
646 (
647 Err(StudyAccessRequestSubmissionPortError::DatastoreUnavailable),
648 SubmitStudyAccessRequestError::DatastoreUnavailable,
649 ),
650 ];
651
652 for (result, expected) in cases {
653 let mut port = FakeSubmissionPort::returning(result);
654 assert_eq!(
655 submit_study_access_request(&mut port, request()).unwrap_err(),
656 expected
657 );
658 assert_eq!(port.received.len(), 1);
659 }
660 }
661
662 #[test]
663 fn requester_lifecycle_use_cases_preserve_requester_scope_and_records() {
664 let pending = requester_record(StudyAccessRequestLifecycle::Pending);
665 let withdrawn = requester_record(StudyAccessRequestLifecycle::Withdrawn);
666 let mut port = requester_port(
667 Ok(vec![pending.clone()]),
668 Ok(Some(pending.clone())),
669 Ok(RequesterStudyAccessRequestWithdrawalOutcome::Withdrawn(
670 withdrawn.clone(),
671 )),
672 );
673 let scope = requester_scope();
674
675 assert_eq!(
676 list_requester_study_access_requests(&mut port, &scope).unwrap(),
677 vec![pending.clone()]
678 );
679 assert_eq!(
680 get_requester_study_access_request(&mut port, &scope, pending.request_id).unwrap(),
681 pending
682 );
683 assert_eq!(
684 withdraw_requester_study_access_request(&mut port, &scope, withdrawn.request_id)
685 .unwrap(),
686 withdrawn
687 );
688 assert_eq!(
689 port.received_scopes,
690 vec![scope.clone(), scope.clone(), scope]
691 );
692 assert_eq!(
693 port.received_request_ids,
694 vec![Uuid::from_u128(3), Uuid::from_u128(3)]
695 );
696 }
697
698 #[test]
699 fn requester_lifecycle_use_cases_preserve_semantic_and_port_failures() {
700 let cases = [
701 (
702 RequesterStudyAccessRequestPortError::InvalidDatastoreIdentity,
703 RequesterStudyAccessRequestError::InvalidDatastoreIdentity,
704 ),
705 (
706 RequesterStudyAccessRequestPortError::AuthorityUnavailable,
707 RequesterStudyAccessRequestError::AuthorityUnavailable,
708 ),
709 (
710 RequesterStudyAccessRequestPortError::DatastoreUnavailable,
711 RequesterStudyAccessRequestError::DatastoreUnavailable,
712 ),
713 (
714 RequesterStudyAccessRequestPortError::InvalidLifecycleState,
715 RequesterStudyAccessRequestError::InvalidLifecycleState,
716 ),
717 ];
718 for (port_error, expected) in cases {
719 let mut port = requester_port(Err(port_error), Err(port_error), Err(port_error));
720 let scope = requester_scope();
721 assert_eq!(
722 list_requester_study_access_requests(&mut port, &scope).unwrap_err(),
723 expected
724 );
725 assert_eq!(
726 get_requester_study_access_request(&mut port, &scope, Uuid::from_u128(3))
727 .unwrap_err(),
728 expected
729 );
730 assert_eq!(
731 withdraw_requester_study_access_request(&mut port, &scope, Uuid::from_u128(3))
732 .unwrap_err(),
733 expected
734 );
735 }
736
737 for (outcome, expected) in [
738 (
739 RequesterStudyAccessRequestWithdrawalOutcome::NotFound,
740 RequesterStudyAccessRequestError::RequestNotFound,
741 ),
742 (
743 RequesterStudyAccessRequestWithdrawalOutcome::NotPending,
744 RequesterStudyAccessRequestError::RequestNotPending,
745 ),
746 ] {
747 let mut port = requester_port(Ok(Vec::new()), Ok(None), Ok(outcome));
748 assert_eq!(
749 withdraw_requester_study_access_request(
750 &mut port,
751 &requester_scope(),
752 Uuid::from_u128(3)
753 )
754 .unwrap_err(),
755 expected
756 );
757 }
758
759 let mut port = requester_port(
760 Ok(Vec::new()),
761 Ok(None),
762 Ok(RequesterStudyAccessRequestWithdrawalOutcome::NotFound),
763 );
764 assert_eq!(
765 get_requester_study_access_request(&mut port, &requester_scope(), Uuid::from_u128(3))
766 .unwrap_err(),
767 RequesterStudyAccessRequestError::RequestNotFound
768 );
769 }
770
771 #[test]
772 fn custodian_use_cases_preserve_scope_and_normalize_rejection_reason() {
773 let pending = custodian_record(StudyAccessRequestLifecycle::Pending);
774 let rejected = CustodianStudyAccessRequest {
775 lifecycle: StudyAccessRequestLifecycle::Rejected,
776 decision: CustodianStudyAccessRequestDecision {
777 decided_at: Some(Utc::now()),
778 decided_by: Some("custodian".to_string()),
779 reason: Some("outside approved purpose".to_string()),
780 },
781 ..pending.clone()
782 };
783 let mut port = custodian_port(
784 Ok(vec![pending.clone()]),
785 Ok(CustodianStudyAccessRequestRejectionOutcome::Rejected(
786 Box::new(rejected.clone()),
787 )),
788 );
789 let scope = custodian_scope();
790
791 assert_eq!(
792 list_custodian_study_access_requests(&mut port, &scope).unwrap(),
793 vec![pending]
794 );
795 assert_eq!(
796 reject_custodian_study_access_request(
797 &mut port,
798 RejectCustodianStudyAccessRequest {
799 datastore_id: scope.datastore_id,
800 request_id: rejected.request_id,
801 reason: Some(" outside approved purpose ".to_string()),
802 },
803 )
804 .unwrap(),
805 rejected
806 );
807 assert_eq!(port.received_scopes, vec![scope]);
808 assert_eq!(
809 port.received_rejections[0].reason.as_deref(),
810 Some("outside approved purpose")
811 );
812 }
813
814 #[test]
815 fn custodian_use_cases_preserve_authorization_lifecycle_and_port_failures() {
816 for (outcome, expected) in [
817 (
818 CustodianStudyAccessRequestRejectionOutcome::NotFound,
819 CustodianStudyAccessRequestError::RequestNotFound,
820 ),
821 (
822 CustodianStudyAccessRequestRejectionOutcome::NotCustodian,
823 CustodianStudyAccessRequestError::CustodianshipRequired,
824 ),
825 (
826 CustodianStudyAccessRequestRejectionOutcome::NotPending,
827 CustodianStudyAccessRequestError::RequestNotPending,
828 ),
829 ] {
830 let mut port = custodian_port(Ok(Vec::new()), Ok(outcome));
831 assert_eq!(
832 reject_custodian_study_access_request(
833 &mut port,
834 RejectCustodianStudyAccessRequest {
835 datastore_id: Uuid::from_u128(1),
836 request_id: Uuid::from_u128(3),
837 reason: Some(" ".to_string()),
838 },
839 )
840 .unwrap_err(),
841 expected
842 );
843 assert_eq!(port.received_rejections[0].reason, None);
844 }
845
846 for (port_error, expected) in [
847 (
848 CustodianStudyAccessRequestPortError::InvalidDatastoreIdentity,
849 CustodianStudyAccessRequestError::InvalidDatastoreIdentity,
850 ),
851 (
852 CustodianStudyAccessRequestPortError::AuthorityUnavailable,
853 CustodianStudyAccessRequestError::AuthorityUnavailable,
854 ),
855 (
856 CustodianStudyAccessRequestPortError::AuthenticatedTreUserRequired,
857 CustodianStudyAccessRequestError::AuthenticatedTreUserRequired,
858 ),
859 (
860 CustodianStudyAccessRequestPortError::CustodianshipRequired,
861 CustodianStudyAccessRequestError::CustodianshipRequired,
862 ),
863 (
864 CustodianStudyAccessRequestPortError::DatastoreUnavailable,
865 CustodianStudyAccessRequestError::DatastoreUnavailable,
866 ),
867 (
868 CustodianStudyAccessRequestPortError::InvalidLifecycleState,
869 CustodianStudyAccessRequestError::InvalidLifecycleState,
870 ),
871 ] {
872 let mut port = custodian_port(Err(port_error), Err(port_error));
873 assert_eq!(
874 list_custodian_study_access_requests(&mut port, &custodian_scope()).unwrap_err(),
875 expected
876 );
877 assert_eq!(
878 reject_custodian_study_access_request(
879 &mut port,
880 RejectCustodianStudyAccessRequest {
881 datastore_id: Uuid::from_u128(1),
882 request_id: Uuid::from_u128(3),
883 reason: None,
884 },
885 )
886 .unwrap_err(),
887 expected
888 );
889 }
890 }
891
892 #[test]
893 fn approval_provisions_only_the_preflighted_request_datastore_then_records_grant() {
894 let approved = CustodianStudyAccessRequest {
895 lifecycle: StudyAccessRequestLifecycle::Approved,
896 decision: CustodianStudyAccessRequestDecision {
897 decided_at: Some(Utc::now()),
898 decided_by: Some("custodian".to_string()),
899 reason: None,
900 },
901 ..custodian_record(StudyAccessRequestLifecycle::Pending)
902 };
903 let approval = ApproveCustodianStudyAccessRequest {
904 datastore_id: Uuid::from_u128(1),
905 request_id: Uuid::from_u128(3),
906 };
907 let mut custodian = FakeCustodianPort {
908 listed: Ok(Vec::new()),
909 rejected: Ok(CustodianStudyAccessRequestRejectionOutcome::NotPending),
910 preflight: VecDeque::from([Ok(
911 CustodianStudyAccessRequestApprovalPreflight::Pending {
912 requester_orcid: "0000-0001-2345-6789".to_string(),
913 },
914 )]),
915 recorded: VecDeque::from([Ok(CustodianStudyAccessRequestApprovalOutcome::Approved(
916 Box::new(approved.clone()),
917 ))]),
918 received_scopes: Vec::new(),
919 received_rejections: Vec::new(),
920 received_approval_preflights: Vec::new(),
921 received_recorded_approvals: Vec::new(),
922 };
923 let mut workflow_entry = FakeWorkflowEntryPort {
924 results: VecDeque::from([Ok("tre_orcid_0000_0001_2345_6789".to_string())]),
925 received: Vec::new(),
926 };
927
928 assert_eq!(
929 approve_custodian_study_access_request(&mut custodian, &mut workflow_entry, approval)
930 .unwrap(),
931 approved
932 );
933 assert_eq!(custodian.received_approval_preflights, vec![approval]);
934 assert_eq!(
935 workflow_entry.received,
936 vec![ProvisionStudyAccessRequestWorkflowEntry {
937 datastore_id: approval.datastore_id,
938 requester_orcid: "0000-0001-2345-6789".to_string(),
939 }]
940 );
941 assert_eq!(
942 custodian.received_recorded_approvals,
943 vec![RecordCustodianStudyAccessRequestApproval {
944 datastore_id: approval.datastore_id,
945 request_id: approval.request_id,
946 requester_role: "tre_orcid_0000_0001_2345_6789".to_string(),
947 }]
948 );
949 }
950
951 #[test]
952 fn approval_does_not_record_a_grant_when_workflow_entry_provisioning_fails() {
953 let approval = ApproveCustodianStudyAccessRequest {
954 datastore_id: Uuid::from_u128(1),
955 request_id: Uuid::from_u128(3),
956 };
957 let mut custodian = FakeCustodianPort {
958 listed: Ok(Vec::new()),
959 rejected: Ok(CustodianStudyAccessRequestRejectionOutcome::NotPending),
960 preflight: VecDeque::from([Ok(
961 CustodianStudyAccessRequestApprovalPreflight::Pending {
962 requester_orcid: "0000-0001-2345-6789".to_string(),
963 },
964 )]),
965 recorded: VecDeque::new(),
966 received_scopes: Vec::new(),
967 received_rejections: Vec::new(),
968 received_approval_preflights: Vec::new(),
969 received_recorded_approvals: Vec::new(),
970 };
971 let mut workflow_entry = FakeWorkflowEntryPort {
972 results: VecDeque::from([Err(
973 StudyAccessRequestWorkflowEntryPortError::DatastoreUnavailable,
974 )]),
975 received: Vec::new(),
976 };
977
978 assert_eq!(
979 approve_custodian_study_access_request(&mut custodian, &mut workflow_entry, approval)
980 .unwrap_err(),
981 CustodianStudyAccessRequestError::DatastoreUnavailable
982 );
983 assert_eq!(custodian.received_approval_preflights, vec![approval]);
984 assert_eq!(workflow_entry.received.len(), 1);
985 assert!(custodian.received_recorded_approvals.is_empty());
986 }
987
988 #[test]
989 fn approval_preserves_preflight_authorization_and_lifecycle_outcomes() {
990 let approval = ApproveCustodianStudyAccessRequest {
991 datastore_id: Uuid::from_u128(1),
992 request_id: Uuid::from_u128(3),
993 };
994 for (preflight, expected) in [
995 (
996 CustodianStudyAccessRequestApprovalPreflight::NotFound,
997 CustodianStudyAccessRequestError::RequestNotFound,
998 ),
999 (
1000 CustodianStudyAccessRequestApprovalPreflight::NotCustodian,
1001 CustodianStudyAccessRequestError::CustodianshipRequired,
1002 ),
1003 (
1004 CustodianStudyAccessRequestApprovalPreflight::NotPending,
1005 CustodianStudyAccessRequestError::RequestNotPending,
1006 ),
1007 ] {
1008 let mut custodian = custodian_port(
1009 Ok(Vec::new()),
1010 Ok(CustodianStudyAccessRequestRejectionOutcome::NotPending),
1011 );
1012 custodian.preflight = VecDeque::from([Ok(preflight)]);
1013 let mut workflow_entry = FakeWorkflowEntryPort {
1014 results: VecDeque::from([Ok("unexpected".to_string())]),
1015 received: Vec::new(),
1016 };
1017
1018 assert_eq!(
1019 approve_custodian_study_access_request(
1020 &mut custodian,
1021 &mut workflow_entry,
1022 approval,
1023 )
1024 .unwrap_err(),
1025 expected
1026 );
1027 assert!(workflow_entry.received.is_empty());
1028 assert!(custodian.received_recorded_approvals.is_empty());
1029 }
1030
1031 let mut invalid_lifecycle = custodian_port(
1032 Ok(Vec::new()),
1033 Ok(CustodianStudyAccessRequestRejectionOutcome::NotPending),
1034 );
1035 invalid_lifecycle.preflight = VecDeque::from([Err(
1036 CustodianStudyAccessRequestPortError::InvalidLifecycleState,
1037 )]);
1038 let mut workflow_entry = FakeWorkflowEntryPort {
1039 results: VecDeque::from([Ok("unexpected".to_string())]),
1040 received: Vec::new(),
1041 };
1042 assert_eq!(
1043 approve_custodian_study_access_request(
1044 &mut invalid_lifecycle,
1045 &mut workflow_entry,
1046 approval,
1047 )
1048 .unwrap_err(),
1049 CustodianStudyAccessRequestError::InvalidLifecycleState
1050 );
1051 assert!(workflow_entry.received.is_empty());
1052 assert!(invalid_lifecycle.received_recorded_approvals.is_empty());
1053 }
1054
1055 #[test]
1056 fn approval_retry_retains_workflow_entry_after_governed_decision_failure() {
1057 let approval = ApproveCustodianStudyAccessRequest {
1058 datastore_id: Uuid::from_u128(1),
1059 request_id: Uuid::from_u128(3),
1060 };
1061 let approved = CustodianStudyAccessRequest {
1062 lifecycle: StudyAccessRequestLifecycle::Approved,
1063 ..custodian_record(StudyAccessRequestLifecycle::Pending)
1064 };
1065 let mut custodian = FakeCustodianPort {
1066 listed: Ok(Vec::new()),
1067 rejected: Ok(CustodianStudyAccessRequestRejectionOutcome::NotPending),
1068 preflight: VecDeque::from([
1069 Ok(CustodianStudyAccessRequestApprovalPreflight::Pending {
1070 requester_orcid: "0000-0001-2345-6789".to_string(),
1071 }),
1072 Ok(CustodianStudyAccessRequestApprovalPreflight::Pending {
1073 requester_orcid: "0000-0001-2345-6789".to_string(),
1074 }),
1075 ]),
1076 recorded: VecDeque::from([
1077 Err(CustodianStudyAccessRequestPortError::DatastoreUnavailable),
1078 Ok(CustodianStudyAccessRequestApprovalOutcome::Approved(
1079 Box::new(approved.clone()),
1080 )),
1081 ]),
1082 received_scopes: Vec::new(),
1083 received_rejections: Vec::new(),
1084 received_approval_preflights: Vec::new(),
1085 received_recorded_approvals: Vec::new(),
1086 };
1087 let mut workflow_entry = FakeWorkflowEntryPort {
1088 results: VecDeque::from([
1089 Ok("tre_orcid_0000_0001_2345_6789".to_string()),
1090 Ok("tre_orcid_0000_0001_2345_6789".to_string()),
1091 ]),
1092 received: Vec::new(),
1093 };
1094
1095 assert_eq!(
1096 approve_custodian_study_access_request(&mut custodian, &mut workflow_entry, approval)
1097 .unwrap_err(),
1098 CustodianStudyAccessRequestError::DatastoreUnavailable
1099 );
1100 assert_eq!(
1101 approve_custodian_study_access_request(&mut custodian, &mut workflow_entry, approval)
1102 .unwrap(),
1103 approved
1104 );
1105 assert_eq!(workflow_entry.received.len(), 2);
1106 assert_eq!(custodian.received_recorded_approvals.len(), 2);
1107 assert!(workflow_entry.results.is_empty());
1108 }
1109}