koprogo_api/application/use_cases/
list_acps_use_case.rs1use crate::application::dto::AcpResponseDto;
20use crate::application::error::AppError;
21use crate::application::ports::AcpRepository;
22use crate::application::use_cases::acp_use_cases::AcpCaller;
23use crate::domain::entities::Acp;
24use std::sync::Arc;
25use uuid::Uuid;
26
27pub struct ListAcpsUseCase {
33 repository: Arc<dyn AcpRepository>,
34}
35
36impl ListAcpsUseCase {
37 pub fn new(repository: Arc<dyn AcpRepository>) -> Self {
38 Self { repository }
39 }
40
41 pub async fn list_for_user(&self, caller: &AcpCaller) -> Result<Vec<AcpResponseDto>, AppError> {
43 let scope = caller.list_scope();
44 let acps = self.repository.list(scope).await?;
45 Ok(acps.iter().map(Self::to_response_dto).collect())
46 }
47
48 pub async fn assert_caller_can_see(
59 &self,
60 caller: &AcpCaller,
61 acp_id: Uuid,
62 ) -> Result<(), AppError> {
63 let acp = self
64 .repository
65 .find_by_id(acp_id)
66 .await?
67 .ok_or(AppError::AcpNotInScope { acp_id })?;
68 Self::caller_can_see(caller, &acp)
69 }
70
71 pub fn caller_can_see(caller: &AcpCaller, acp: &Acp) -> Result<(), AppError> {
74 match caller {
75 AcpCaller::SuperAdmin => Ok(()),
76 AcpCaller::Admin { organization_id } | AcpCaller::Syndic { organization_id } => {
77 match acp.organization_id {
78 Some(org) if org == *organization_id => Ok(()),
79 _ => Err(AppError::AcpNotInScope { acp_id: acp.id }),
80 }
81 }
82 AcpCaller::Owner { .. } => Err(AppError::AcpNotInScope { acp_id: acp.id }),
90 }
91 }
92
93 fn to_response_dto(acp: &Acp) -> AcpResponseDto {
94 AcpResponseDto {
95 id: acp.id.to_string(),
96 organization_id: acp.organization_id.map(|u| u.to_string()),
97 name: acp.name.clone(),
98 slug: acp.slug.clone(),
99 legal_status: acp.legal_status.as_db_str().to_string(),
100 total_tantiemes: acp.total_tantiemes,
101 bce_number: acp.bce_number.clone(),
102 address_street: acp.address_street.clone(),
103 address_postal_code: acp.address_postal_code.clone(),
104 address_city: acp.address_city.clone(),
105 created_at: acp.created_at.to_rfc3339(),
106 updated_at: acp.updated_at.to_rfc3339(),
107 }
108 }
109}
110
111#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::application::ports::{AcpRepository, ListScope};
119 use crate::domain::entities::Acp;
120 use async_trait::async_trait;
121 use mockall::mock;
122
123 mock! {
124 AcpRepo {}
125
126 #[async_trait]
127 impl AcpRepository for AcpRepo {
128 async fn create(&self, acp: &Acp) -> Result<Acp, AppError>;
129 async fn find_by_id(&self, id: Uuid) -> Result<Option<Acp>, AppError>;
130 async fn find_by_id_with_metrics(&self, id: Uuid) -> Result<Option<(Acp, crate::domain::entities::AcpMetrics)>, AppError>;
131 async fn list(&self, scope: ListScope) -> Result<Vec<Acp>, AppError>;
132 async fn list_with_metrics(
133 &self,
134 scope: ListScope,
135 ) -> Result<Vec<(Acp, crate::domain::entities::AcpMetrics)>, AppError>;
136 async fn update(&self, acp: &Acp) -> Result<Acp, AppError>;
137 async fn archive(&self, id: Uuid) -> Result<(), AppError>;
138 async fn count_buildings(&self, id: Uuid) -> Result<i64, AppError>;
139 }
140 }
141
142 fn acp(org_id: Option<Uuid>, name: &str) -> Acp {
143 Acp::new(
144 org_id,
145 name.to_string(),
146 "Rue X 1".to_string(),
147 "1000".to_string(),
148 "Bruxelles".to_string(),
149 None,
150 )
151 .expect("valid acp")
152 }
153
154 #[tokio::test]
157 async fn happy_super_admin_lists_all_acps() {
158 let org_a = Uuid::new_v4();
159 let org_b = Uuid::new_v4();
160 let acps = vec![acp(Some(org_a), "Acp A1"), acp(Some(org_b), "Acp B1")];
161
162 let mut repo = MockAcpRepo::new();
163 let acps_clone = acps.clone();
164 repo.expect_list()
165 .withf(|s| matches!(s, ListScope::All))
166 .returning(move |_| Ok(acps_clone.clone()));
167
168 let uc = ListAcpsUseCase::new(Arc::new(repo));
169 let list = uc.list_for_user(&AcpCaller::SuperAdmin).await.expect("ok");
170 assert_eq!(list.len(), 2);
171 }
172
173 #[tokio::test]
174 async fn happy_syndic_lists_only_own_cabinet() {
175 let org_a = Uuid::new_v4();
176 let mut repo = MockAcpRepo::new();
177 repo.expect_list()
178 .withf(move |s| matches!(s, ListScope::Organization(o) if *o == org_a))
179 .returning(move |_| Ok(vec![acp(Some(org_a), "A1"), acp(Some(org_a), "A2")]));
180
181 let uc = ListAcpsUseCase::new(Arc::new(repo));
182 let list = uc
183 .list_for_user(&AcpCaller::Syndic {
184 organization_id: org_a,
185 })
186 .await
187 .expect("ok");
188 assert_eq!(list.len(), 2);
189 }
190
191 #[tokio::test]
194 async fn edge_owner_with_no_assignment_sees_empty_list() {
195 let user_id = Uuid::new_v4();
196 let mut repo = MockAcpRepo::new();
197 repo.expect_list()
198 .withf(move |s| matches!(s, ListScope::Owner(u) if *u == user_id))
199 .returning(|_| Ok(vec![]));
200
201 let uc = ListAcpsUseCase::new(Arc::new(repo));
202 let list = uc
203 .list_for_user(&AcpCaller::Owner { user_id })
204 .await
205 .expect("ok");
206 assert!(list.is_empty());
207 }
208
209 #[tokio::test]
212 async fn security_syndic_cannot_see_acp_of_other_cabinet() {
213 let cabinet_a = Uuid::new_v4();
214 let cabinet_b = Uuid::new_v4();
215 let target = acp(Some(cabinet_a), "Foreign Acp");
216 let target_id = target.id;
217
218 let mut repo = MockAcpRepo::new();
219 let target_clone = target.clone();
220 repo.expect_find_by_id()
221 .returning(move |_| Ok(Some(target_clone.clone())));
222
223 let uc = ListAcpsUseCase::new(Arc::new(repo));
224 let err = uc
225 .assert_caller_can_see(
226 &AcpCaller::Syndic {
227 organization_id: cabinet_b,
228 },
229 target_id,
230 )
231 .await
232 .unwrap_err();
233 match err {
234 AppError::AcpNotInScope { acp_id } => assert_eq!(acp_id, target_id),
235 other => panic!("expected AcpNotInScope, got {:?}", other),
236 }
237 }
238
239 #[tokio::test]
240 async fn security_owner_cannot_pin_arbitrary_acp_via_scope_header() {
241 let user_id = Uuid::new_v4();
242 let target = acp(Some(Uuid::new_v4()), "Some Acp");
243 let target_id = target.id;
244 let mut repo = MockAcpRepo::new();
245 let target_clone = target.clone();
246 repo.expect_find_by_id()
247 .returning(move |_| Ok(Some(target_clone.clone())));
248
249 let uc = ListAcpsUseCase::new(Arc::new(repo));
250 let err = uc
251 .assert_caller_can_see(&AcpCaller::Owner { user_id }, target_id)
252 .await
253 .unwrap_err();
254 assert!(matches!(err, AppError::AcpNotInScope { .. }));
255 }
256
257 #[tokio::test]
260 async fn negative_assert_unknown_acp_returns_acp_not_in_scope() {
261 let mut repo = MockAcpRepo::new();
262 repo.expect_find_by_id().returning(|_| Ok(None));
263 let uc = ListAcpsUseCase::new(Arc::new(repo));
264 let err = uc
265 .assert_caller_can_see(&AcpCaller::SuperAdmin, Uuid::new_v4())
266 .await
267 .unwrap_err();
268 assert!(matches!(err, AppError::AcpNotInScope { .. }));
269 }
270}