Skip to main content

ahri_tre_app/
access_request.rs

1//! Study access-request workflows.
2//!
3//! This module owns the application semantics of a browser visitor submitting
4//! a governance request. Transport adapters supply the selected datastore and
5//! requester identity, while persistence adapters implement the narrow port.
6
7use 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/// Application failures for submitting a Study access request.
28#[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/// Application failures for requester-owned access-request lifecycle actions.
45#[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/// Application failures for custodian-owned access-request lifecycle actions.
62#[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
82/// Submits a pending Study access request without granting Study access or
83/// datastore workflow entry.
84pub 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
121/// Lists access requests owned by the current requester in the selected
122/// datastore.
123pub 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
131/// Finds an access request only when it is owned by the current requester in
132/// the selected datastore.
133pub 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
143/// Withdraws a pending access request only when it is owned by the current
144/// requester in the selected datastore. This workflow cannot grant access or
145/// provision datastore workflow entry.
146pub 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
184/// Lists Study access requests in one supported status that the authenticated
185/// database actor is authorized to steward in the selected datastore.
186pub 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
194/// Rejects a pending Study access request when the authenticated database
195/// actor custodians its Study. Empty rejection reasons are normalized to none.
196pub 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
218/// Approves a pending Study access request in three ordered steps: verify the
219/// authenticated custodian's scoped request, provision or reuse requester
220/// workflow entry in that request's datastore, then record the governed
221/// Study-access grant and decision. If the final step fails, workflow entry is
222/// deliberately retained; a retry safely provisions it again and re-runs only
223/// the governed decision without attempting compensation.
224pub 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}