koprogo_api/application/use_cases/
security_incident_use_cases.rs1use crate::application::ports::security_incident_repository::{
2 SecurityIncidentFilters, SecurityIncidentRepository,
3};
4use crate::domain::entities::SecurityIncident;
5use std::sync::Arc;
6use uuid::Uuid;
7
8pub struct SecurityIncidentUseCases {
9 repository: Arc<dyn SecurityIncidentRepository>,
10}
11
12impl SecurityIncidentUseCases {
13 pub fn new(repository: Arc<dyn SecurityIncidentRepository>) -> Self {
14 Self { repository }
15 }
16
17 pub async fn create(
18 &self,
19 organization_id: Option<Uuid>,
20 reported_by: Uuid,
21 severity: String,
22 incident_type: String,
23 title: String,
24 description: String,
25 data_categories_affected: Vec<String>,
26 affected_subjects_count: Option<i32>,
27 ) -> Result<SecurityIncident, String> {
28 let incident = SecurityIncident::new(
29 organization_id,
30 reported_by,
31 severity,
32 incident_type,
33 title,
34 description,
35 data_categories_affected,
36 affected_subjects_count,
37 )?;
38 self.repository.create(&incident).await
39 }
40
41 pub async fn find_all(
42 &self,
43 organization_id: Option<Uuid>,
44 severity: Option<String>,
45 status: Option<String>,
46 page: i64,
47 per_page: i64,
48 ) -> Result<(Vec<SecurityIncident>, i64), String> {
49 let filters = SecurityIncidentFilters {
50 severity,
51 status,
52 page,
53 per_page,
54 };
55 self.repository.find_all(organization_id, filters).await
56 }
57
58 pub async fn find_by_id(
59 &self,
60 id: Uuid,
61 organization_id: Option<Uuid>,
62 ) -> Result<Option<SecurityIncident>, String> {
63 self.repository.find_by_id(id, organization_id).await
64 }
65
66 pub async fn report_to_apd(
67 &self,
68 id: Uuid,
69 organization_id: Option<Uuid>,
70 apd_reference_number: String,
71 investigation_notes: Option<String>,
72 ) -> Result<Option<SecurityIncident>, String> {
73 if apd_reference_number.is_empty() {
74 return Err("apd_reference_number is required".to_string());
75 }
76 match self.repository.find_by_id(id, organization_id).await? {
78 None => Ok(None),
79 Some(incident) if incident.notification_at.is_some() => {
80 Err("already_reported".to_string())
81 }
82 Some(_) => {
83 self.repository
84 .report_to_apd(
85 id,
86 organization_id,
87 apd_reference_number,
88 investigation_notes,
89 )
90 .await
91 }
92 }
93 }
94
95 pub async fn find_overdue(
96 &self,
97 organization_id: Option<Uuid>,
98 ) -> Result<Vec<SecurityIncident>, String> {
99 self.repository.find_overdue(organization_id).await
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::application::ports::security_incident_repository::SecurityIncidentFilters;
107 use async_trait::async_trait;
108
109 struct MockRepo {
110 should_fail: bool,
111 }
112
113 #[async_trait]
114 impl SecurityIncidentRepository for MockRepo {
115 async fn create(&self, incident: &SecurityIncident) -> Result<SecurityIncident, String> {
116 if self.should_fail {
117 return Err("db error".to_string());
118 }
119 Ok(incident.clone())
120 }
121
122 async fn find_by_id(
123 &self,
124 _id: Uuid,
125 _org: Option<Uuid>,
126 ) -> Result<Option<SecurityIncident>, String> {
127 Ok(None)
128 }
129
130 async fn find_all(
131 &self,
132 _org: Option<Uuid>,
133 _filters: SecurityIncidentFilters,
134 ) -> Result<(Vec<SecurityIncident>, i64), String> {
135 Ok((vec![], 0))
136 }
137
138 async fn report_to_apd(
139 &self,
140 _id: Uuid,
141 _org: Option<Uuid>,
142 _ref: String,
143 _notes: Option<String>,
144 ) -> Result<Option<SecurityIncident>, String> {
145 Ok(None)
146 }
147
148 async fn find_overdue(&self, _org: Option<Uuid>) -> Result<Vec<SecurityIncident>, String> {
149 Ok(vec![])
150 }
151 }
152
153 #[tokio::test]
154 async fn test_create_validates_domain() {
155 let uc = SecurityIncidentUseCases::new(Arc::new(MockRepo { should_fail: false }));
156 let result = uc
158 .create(
159 None,
160 Uuid::new_v4(),
161 "high".to_string(),
162 "breach".to_string(),
163 "".to_string(),
164 "desc".to_string(),
165 vec![],
166 None,
167 )
168 .await;
169 assert!(result.is_err());
170 assert!(result.unwrap_err().contains("title"));
171 }
172
173 #[tokio::test]
174 async fn test_create_invalid_severity_rejected() {
175 let uc = SecurityIncidentUseCases::new(Arc::new(MockRepo { should_fail: false }));
176 let result = uc
177 .create(
178 None,
179 Uuid::new_v4(),
180 "extreme".to_string(),
181 "breach".to_string(),
182 "title".to_string(),
183 "desc".to_string(),
184 vec![],
185 None,
186 )
187 .await;
188 assert!(result.is_err());
189 }
190
191 #[tokio::test]
192 async fn test_create_success() {
193 let uc = SecurityIncidentUseCases::new(Arc::new(MockRepo { should_fail: false }));
194 let result = uc
195 .create(
196 Some(Uuid::new_v4()),
197 Uuid::new_v4(),
198 "critical".to_string(),
199 "data_breach".to_string(),
200 "Production DB leaked".to_string(),
201 "Details here".to_string(),
202 vec!["email".to_string(), "address".to_string()],
203 Some(500),
204 )
205 .await;
206 assert!(result.is_ok());
207 let inc = result.unwrap();
208 assert_eq!(inc.status, "detected");
209 assert_eq!(inc.severity, "critical");
210 }
211
212 #[tokio::test]
213 async fn test_report_to_apd_empty_ref_rejected() {
214 let uc = SecurityIncidentUseCases::new(Arc::new(MockRepo { should_fail: false }));
215 let result = uc
216 .report_to_apd(Uuid::new_v4(), None, "".to_string(), None)
217 .await;
218 assert!(result.is_err());
219 assert!(result.unwrap_err().contains("required"));
220 }
221
222 #[tokio::test]
223 async fn test_find_all_empty() {
224 let uc = SecurityIncidentUseCases::new(Arc::new(MockRepo { should_fail: false }));
225 let (incidents, total) = uc.find_all(None, None, None, 1, 20).await.unwrap();
226 assert_eq!(total, 0);
227 assert!(incidents.is_empty());
228 }
229}