1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use serde_json::{Value, json};
6use winterbaume_core::{
7 BackendState, DEFAULT_ACCOUNT_ID, MockRequest, MockResponse, MockService, StateChangeNotifier,
8 StatefulService, json_error_response,
9};
10
11use crate::state::{CodeBuildError, CodeBuildState};
12use crate::views::CodeBuildStateView;
13use crate::wire;
14
15pub struct CodeBuildService {
16 pub(crate) state: Arc<BackendState<CodeBuildState>>,
17 pub(crate) notifier: StateChangeNotifier<CodeBuildStateView>,
18}
19
20impl CodeBuildService {
21 pub fn new() -> Self {
22 Self {
23 state: Arc::new(BackendState::new()),
24 notifier: StateChangeNotifier::new(),
25 }
26 }
27}
28
29impl Default for CodeBuildService {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl MockService for CodeBuildService {
36 fn service_name(&self) -> &str {
37 "codebuild"
38 }
39
40 fn url_patterns(&self) -> Vec<&str> {
41 vec![
42 r"https?://codebuild\..*\.amazonaws\.com",
43 r"https?://codebuild\.amazonaws\.com",
44 ]
45 }
46
47 fn handle(
48 &self,
49 request: MockRequest,
50 ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
51 Box::pin(async move { self.dispatch(request).await })
52 }
53}
54
55impl CodeBuildService {
56 async fn dispatch(&self, request: MockRequest) -> MockResponse {
57 let region = winterbaume_core::auth::extract_region_from_uri(&request.uri);
58 let account_id = DEFAULT_ACCOUNT_ID;
59
60 let action = request
61 .headers
62 .get("x-amz-target")
63 .and_then(|v| v.to_str().ok())
64 .and_then(|v| v.split('.').next_back())
65 .map(|s| s.to_string());
66
67 let action = match action {
68 Some(a) => a,
69 None => {
70 return json_error_response(400, "MissingAction", "Missing X-Amz-Target header");
71 }
72 };
73
74 if serde_json::from_slice::<Value>(&request.body).is_err() {
77 return json_error_response(400, "SerializationException", "Invalid JSON body");
78 }
79 let body_bytes: &[u8] = &request.body;
80
81 let state = self.state.get(account_id, ®ion);
82
83 let response = match action.as_str() {
84 "CreateProject" => {
85 self.handle_create_project(&state, body_bytes, account_id, ®ion)
86 .await
87 }
88 "BatchGetProjects" => self.handle_batch_get_projects(&state, body_bytes).await,
89 "DeleteProject" => self.handle_delete_project(&state, body_bytes).await,
90 "ListProjects" => self.handle_list_projects(&state).await,
91 "BatchDeleteBuilds" => self.handle_batch_delete_builds(&state, body_bytes).await,
92 "BatchGetBuilds" => self.handle_batch_get_builds(&state, body_bytes).await,
93 "UpdateProject" => {
94 self.handle_update_project(&state, body_bytes, account_id)
95 .await
96 }
97 "RetryBuild" => {
98 self.handle_retry_build(&state, body_bytes, account_id, ®ion)
99 .await
100 }
101 "CreateWebhook" => {
102 self.handle_create_webhook(&state, body_bytes, account_id, ®ion)
103 .await
104 }
105 "UpdateWebhook" => self.handle_update_webhook(&state, body_bytes).await,
106 "DeleteWebhook" => self.handle_delete_webhook(&state, body_bytes).await,
107 "ImportSourceCredentials" => {
108 self.handle_import_source_credentials(&state, body_bytes, account_id, ®ion)
109 .await
110 }
111 "ListSourceCredentials" => self.handle_list_source_credentials(&state).await,
112 "DeleteSourceCredentials" => {
113 self.handle_delete_source_credentials(&state, body_bytes)
114 .await
115 }
116 "PutResourcePolicy" => self.handle_put_resource_policy(&state, body_bytes).await,
117 "GetResourcePolicy" => self.handle_get_resource_policy(&state, body_bytes).await,
118 "DeleteResourcePolicy" => self.handle_delete_resource_policy(&state, body_bytes).await,
119 "InvalidateProjectCache" => {
120 self.handle_invalidate_project_cache(&state, body_bytes)
121 .await
122 }
123 "DescribeTestCases" => {
126 wire::serialize_describe_test_cases_response(&wire::DescribeTestCasesOutput {
127 ..Default::default()
128 })
129 }
130 "ListReportGroups" => self.handle_list_report_groups(&state).await,
131 "BatchGetBuildBatches" => json_error_response(
133 501,
134 "NotImplementedError",
135 "BatchGetBuildBatches is not yet implemented in winterbaume-codebuild",
136 ),
137 "BatchGetCommandExecutions" => json_error_response(
138 501,
139 "NotImplementedError",
140 "BatchGetCommandExecutions is not yet implemented in winterbaume-codebuild",
141 ),
142 "BatchGetFleets" => json_error_response(
143 501,
144 "NotImplementedError",
145 "BatchGetFleets is not yet implemented in winterbaume-codebuild",
146 ),
147 "BatchGetReportGroups" => {
148 self.handle_batch_get_report_groups(&state, body_bytes)
149 .await
150 }
151 "BatchGetReports" => json_error_response(
152 501,
153 "NotImplementedError",
154 "BatchGetReports is not yet implemented in winterbaume-codebuild",
155 ),
156 "BatchGetSandboxes" => json_error_response(
157 501,
158 "NotImplementedError",
159 "BatchGetSandboxes is not yet implemented in winterbaume-codebuild",
160 ),
161 "CreateFleet" => json_error_response(
162 501,
163 "NotImplementedError",
164 "CreateFleet is not yet implemented in winterbaume-codebuild",
165 ),
166 "CreateReportGroup" => {
167 self.handle_create_report_group(&state, body_bytes, account_id, ®ion)
168 .await
169 }
170 "DeleteBuildBatch" => json_error_response(
171 501,
172 "NotImplementedError",
173 "DeleteBuildBatch is not yet implemented in winterbaume-codebuild",
174 ),
175 "DeleteFleet" => json_error_response(
176 501,
177 "NotImplementedError",
178 "DeleteFleet is not yet implemented in winterbaume-codebuild",
179 ),
180 "DeleteReport" => json_error_response(
181 501,
182 "NotImplementedError",
183 "DeleteReport is not yet implemented in winterbaume-codebuild",
184 ),
185 "DeleteReportGroup" => self.handle_delete_report_group(&state, body_bytes).await,
186 "DescribeCodeCoverages" => json_error_response(
187 501,
188 "NotImplementedError",
189 "DescribeCodeCoverages is not yet implemented in winterbaume-codebuild",
190 ),
191 "GetReportGroupTrend" => json_error_response(
192 501,
193 "NotImplementedError",
194 "GetReportGroupTrend is not yet implemented in winterbaume-codebuild",
195 ),
196 "ListBuildBatches" => json_error_response(
197 501,
198 "NotImplementedError",
199 "ListBuildBatches is not yet implemented in winterbaume-codebuild",
200 ),
201 "ListBuildBatchesForProject" => json_error_response(
202 501,
203 "NotImplementedError",
204 "ListBuildBatchesForProject is not yet implemented in winterbaume-codebuild",
205 ),
206 "ListBuilds" => self.handle_list_builds(&state).await,
207 "ListBuildsForProject" => {
208 self.handle_list_builds_for_project(&state, body_bytes, account_id, ®ion)
209 .await
210 }
211 "ListCommandExecutionsForSandbox" => json_error_response(
212 501,
213 "NotImplementedError",
214 "ListCommandExecutionsForSandbox is not yet implemented in winterbaume-codebuild",
215 ),
216 "ListCuratedEnvironmentImages" => json_error_response(
217 501,
218 "NotImplementedError",
219 "ListCuratedEnvironmentImages is not yet implemented in winterbaume-codebuild",
220 ),
221 "ListFleets" => json_error_response(
222 501,
223 "NotImplementedError",
224 "ListFleets is not yet implemented in winterbaume-codebuild",
225 ),
226 "ListReports" => json_error_response(
227 501,
228 "NotImplementedError",
229 "ListReports is not yet implemented in winterbaume-codebuild",
230 ),
231 "ListReportsForReportGroup" => {
232 self.handle_list_reports_for_report_group(&state, body_bytes)
233 .await
234 }
235 "ListSandboxes" => json_error_response(
236 501,
237 "NotImplementedError",
238 "ListSandboxes is not yet implemented in winterbaume-codebuild",
239 ),
240 "ListSandboxesForProject" => json_error_response(
241 501,
242 "NotImplementedError",
243 "ListSandboxesForProject is not yet implemented in winterbaume-codebuild",
244 ),
245 "ListSharedProjects" => json_error_response(
246 501,
247 "NotImplementedError",
248 "ListSharedProjects is not yet implemented in winterbaume-codebuild",
249 ),
250 "ListSharedReportGroups" => json_error_response(
251 501,
252 "NotImplementedError",
253 "ListSharedReportGroups is not yet implemented in winterbaume-codebuild",
254 ),
255 "RetryBuildBatch" => json_error_response(
256 501,
257 "NotImplementedError",
258 "RetryBuildBatch is not yet implemented in winterbaume-codebuild",
259 ),
260 "StartBuild" => {
261 self.handle_start_build(&state, body_bytes, account_id, ®ion)
262 .await
263 }
264 "StartBuildBatch" => json_error_response(
265 501,
266 "NotImplementedError",
267 "StartBuildBatch is not yet implemented in winterbaume-codebuild",
268 ),
269 "StartCommandExecution" => json_error_response(
270 501,
271 "NotImplementedError",
272 "StartCommandExecution is not yet implemented in winterbaume-codebuild",
273 ),
274 "StartSandbox" => json_error_response(
275 501,
276 "NotImplementedError",
277 "StartSandbox is not yet implemented in winterbaume-codebuild",
278 ),
279 "StartSandboxConnection" => json_error_response(
280 501,
281 "NotImplementedError",
282 "StartSandboxConnection is not yet implemented in winterbaume-codebuild",
283 ),
284 "StopBuild" => self.handle_stop_build(&state, body_bytes).await,
285 "StopBuildBatch" => json_error_response(
286 501,
287 "NotImplementedError",
288 "StopBuildBatch is not yet implemented in winterbaume-codebuild",
289 ),
290 "StopSandbox" => json_error_response(
291 501,
292 "NotImplementedError",
293 "StopSandbox is not yet implemented in winterbaume-codebuild",
294 ),
295 "UpdateFleet" => json_error_response(
296 501,
297 "NotImplementedError",
298 "UpdateFleet is not yet implemented in winterbaume-codebuild",
299 ),
300 "UpdateProjectVisibility" => json_error_response(
301 501,
302 "NotImplementedError",
303 "UpdateProjectVisibility is not yet implemented in winterbaume-codebuild",
304 ),
305 "UpdateReportGroup" => self.handle_update_report_group(&state, body_bytes).await,
306 _ => json_error_response(400, "InvalidAction", &format!("Unknown operation {action}")),
307 };
308
309 if response.status / 100 == 2 {
310 self.notify_state_changed(account_id, ®ion).await;
311 }
312 response
313 }
314
315 async fn handle_create_project(
316 &self,
317 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
318 body: &[u8],
319 account_id: &str,
320 region: &str,
321 ) -> MockResponse {
322 let input = match wire::deserialize_create_project_request(body) {
323 Ok(v) => v,
324 Err(e) => return json_error_response(400, "ValidationException", &e),
325 };
326 if input.name.is_empty() {
327 return json_error_response(400, "InvalidInputException", "name is required");
328 }
329
330 let description = input.description.unwrap_or_default();
331 let source_type = if input.source.r#type.is_empty() {
332 "NO_SOURCE".to_string()
333 } else {
334 input.source.r#type.clone()
335 };
336 let source_location = input.source.location.unwrap_or_default();
337 let artifact_type = if input.artifacts.r#type.is_empty() {
338 "NO_ARTIFACTS".to_string()
339 } else {
340 input.artifacts.r#type.clone()
341 };
342 let artifact_location = input.artifacts.location;
343 let env_type = if input.environment.r#type.is_empty() {
344 "LINUX_CONTAINER".to_string()
345 } else {
346 input.environment.r#type.clone()
347 };
348 let env_image = if input.environment.image.is_empty() {
349 "aws/codebuild/standard:5.0".to_string()
350 } else {
351 input.environment.image.clone()
352 };
353 let env_compute = if input.environment.compute_type.is_empty() {
354 "BUILD_GENERAL1_SMALL".to_string()
355 } else {
356 input.environment.compute_type.clone()
357 };
358 let service_role = input.service_role;
359 let tags = tags_from_wire(input.tags.unwrap_or_default());
360
361 let mut state = state.write().await;
362 match state.create_project(
363 &input.name,
364 &description,
365 &source_type,
366 &source_location,
367 &artifact_type,
368 artifact_location.as_deref(),
369 &env_type,
370 &env_image,
371 &env_compute,
372 &service_role,
373 tags,
374 account_id,
375 region,
376 ) {
377 Ok(project) => wire::serialize_create_project_response(&wire::CreateProjectOutput {
378 project: Some(project_to_wire(project)),
379 }),
380 Err(e) => codebuild_error_response(&e),
381 }
382 }
383
384 async fn handle_batch_get_projects(
385 &self,
386 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
387 body: &[u8],
388 ) -> MockResponse {
389 let input = match wire::deserialize_batch_get_projects_request(body) {
390 Ok(v) => v,
391 Err(e) => return json_error_response(400, "ValidationException", &e),
392 };
393 let names = input.names;
394
395 let state = state.read().await;
396 let projects = state.batch_get_projects(&names);
397 let found: Vec<wire::Project> = projects.iter().map(|p| project_to_wire(p)).collect();
398 let found_names: Vec<&str> = projects.iter().map(|p| p.name.as_str()).collect();
399 let found_arns: Vec<&str> = projects.iter().map(|p| p.arn.as_str()).collect();
400 let not_found: Vec<String> = names
401 .iter()
402 .filter(|n| !found_names.contains(&n.as_str()) && !found_arns.contains(&n.as_str()))
403 .cloned()
404 .collect();
405
406 wire::serialize_batch_get_projects_response(&wire::BatchGetProjectsOutput {
407 projects: Some(found),
408 projects_not_found: Some(not_found),
409 })
410 }
411
412 async fn handle_delete_project(
413 &self,
414 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
415 body: &[u8],
416 ) -> MockResponse {
417 let input = match wire::deserialize_delete_project_request(body) {
418 Ok(v) => v,
419 Err(e) => return json_error_response(400, "ValidationException", &e),
420 };
421 if input.name.is_empty() {
422 return json_error_response(400, "InvalidInputException", "name is required");
423 }
424
425 let mut state = state.write().await;
426 match state.delete_project(&input.name) {
427 Ok(()) => wire::serialize_delete_project_response(&wire::DeleteProjectOutput {}),
428 Err(e) => codebuild_error_response(&e),
429 }
430 }
431
432 async fn handle_batch_get_builds(
433 &self,
434 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
435 body: &[u8],
436 ) -> MockResponse {
437 let input = match wire::deserialize_batch_get_builds_request(body) {
438 Ok(v) => v,
439 Err(e) => return json_error_response(400, "ValidationException", &e),
440 };
441 let ids = input.ids;
442
443 let state = state.read().await;
444 let builds = match state.batch_get_builds(&ids) {
445 Ok(b) => b,
446 Err(e) => return codebuild_error_response(&e),
447 };
448 let found_ids: Vec<&str> = builds.iter().map(|b| b.id.as_str()).collect();
449 let not_found: Vec<String> = ids
450 .iter()
451 .filter(|id| !found_ids.contains(&id.as_str()))
452 .cloned()
453 .collect();
454
455 let found: Vec<wire::Build> = builds.iter().map(build_to_wire).collect();
456
457 wire::serialize_batch_get_builds_response(&wire::BatchGetBuildsOutput {
458 builds: Some(found),
459 builds_not_found: Some(not_found),
460 })
461 }
462
463 async fn handle_list_projects(
464 &self,
465 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
466 ) -> MockResponse {
467 let state = state.read().await;
468 let names: Vec<String> = state
469 .list_projects()
470 .into_iter()
471 .map(|s| s.to_string())
472 .collect();
473
474 wire::serialize_list_projects_response(&wire::ListProjectsOutput {
475 next_token: None,
476 projects: Some(names),
477 })
478 }
479
480 async fn handle_start_build(
481 &self,
482 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
483 body: &[u8],
484 account_id: &str,
485 region: &str,
486 ) -> MockResponse {
487 let input = match wire::deserialize_start_build_request(body) {
488 Ok(v) => v,
489 Err(e) => return json_error_response(400, "ValidationException", &e),
490 };
491 if input.project_name.is_empty() {
492 return json_error_response(400, "InvalidInputException", "projectName is required");
493 }
494
495 let mut state = state.write().await;
496 match state.start_build(
497 &input.project_name,
498 input.source_version.as_deref(),
499 account_id,
500 region,
501 ) {
502 Ok(build) => wire::serialize_start_build_response(&wire::StartBuildOutput {
503 build: Some(build_to_wire(build)),
504 }),
505 Err(e) => codebuild_error_response(&e),
506 }
507 }
508
509 async fn handle_stop_build(
510 &self,
511 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
512 body: &[u8],
513 ) -> MockResponse {
514 let input = match wire::deserialize_stop_build_request(body) {
515 Ok(v) => v,
516 Err(e) => return json_error_response(400, "ValidationException", &e),
517 };
518 if input.id.is_empty() {
519 return json_error_response(400, "InvalidInputException", "id is required");
520 }
521
522 let mut state = state.write().await;
523 match state.stop_build(&input.id) {
524 Ok(build) => wire::serialize_stop_build_response(&wire::StopBuildOutput {
525 build: Some(build_to_wire(build)),
526 }),
527 Err(e) => codebuild_error_response(&e),
528 }
529 }
530
531 async fn handle_list_builds(
532 &self,
533 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
534 ) -> MockResponse {
535 let state = state.read().await;
536 let ids: Vec<String> = state
537 .list_builds()
538 .into_iter()
539 .map(|s| s.to_string())
540 .collect();
541
542 wire::serialize_list_builds_response(&wire::ListBuildsOutput {
543 ids: Some(ids),
544 next_token: None,
545 })
546 }
547
548 async fn handle_list_builds_for_project(
549 &self,
550 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
551 body: &[u8],
552 account_id: &str,
553 region: &str,
554 ) -> MockResponse {
555 let input = match wire::deserialize_list_builds_for_project_request(body) {
556 Ok(v) => v,
557 Err(e) => return json_error_response(400, "ValidationException", &e),
558 };
559 if input.project_name.is_empty() {
560 return json_error_response(400, "InvalidInputException", "projectName is required");
561 }
562
563 let state = state.read().await;
564 if !state.projects.contains_key(&input.project_name) {
565 let project_name = &input.project_name;
566 return json_error_response(
567 400,
568 "ResourceNotFoundException",
569 &format!(
570 "The provided project arn:aws:codebuild:{region}:{account_id}:project/{project_name} does not exist"
571 ),
572 );
573 }
574 let ids: Vec<String> = state
575 .list_builds_for_project(&input.project_name)
576 .into_iter()
577 .map(|s| s.to_string())
578 .collect();
579
580 wire::serialize_list_builds_for_project_response(&wire::ListBuildsForProjectOutput {
581 ids: Some(ids),
582 next_token: None,
583 })
584 }
585
586 async fn handle_batch_delete_builds(
587 &self,
588 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
589 body: &[u8],
590 ) -> MockResponse {
591 let input = match wire::deserialize_batch_delete_builds_request(body) {
592 Ok(v) => v,
593 Err(e) => return json_error_response(400, "ValidationException", &e),
594 };
595
596 let mut state = state.write().await;
597 let deleted = state.batch_delete_builds(&input.ids);
598
599 wire::serialize_batch_delete_builds_response(&wire::BatchDeleteBuildsOutput {
600 builds_deleted: Some(deleted),
601 builds_not_deleted: None,
602 })
603 }
604
605 async fn handle_update_project(
606 &self,
607 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
608 body: &[u8],
609 account_id: &str,
610 ) -> MockResponse {
611 let input = match wire::deserialize_update_project_request(body) {
612 Ok(v) => v,
613 Err(e) => return json_error_response(400, "ValidationException", &e),
614 };
615 if input.name.is_empty() {
616 return json_error_response(400, "InvalidInputException", "name is required");
617 }
618
619 let description = input.description;
620 let source_type = input
621 .source
622 .as_ref()
623 .map(|s| s.r#type.clone())
624 .filter(|t| !t.is_empty());
625 let source_location = input.source.as_ref().and_then(|s| s.location.clone());
626 let artifact_type = input
627 .artifacts
628 .as_ref()
629 .map(|a| a.r#type.clone())
630 .filter(|t| !t.is_empty());
631 let artifact_location: Option<Option<String>> =
633 input.artifacts.as_ref().map(|a| a.location.clone());
634 let env_type = input
635 .environment
636 .as_ref()
637 .map(|e| e.r#type.clone())
638 .filter(|t| !t.is_empty());
639 let env_image = input
640 .environment
641 .as_ref()
642 .map(|e| e.image.clone())
643 .filter(|t| !t.is_empty());
644 let env_compute = input
645 .environment
646 .as_ref()
647 .map(|e| e.compute_type.clone())
648 .filter(|t| !t.is_empty());
649 let service_role = input.service_role;
650 let tags = input.tags.map(tags_from_wire);
651
652 let mut state = state.write().await;
653 match state.update_project(
654 &input.name,
655 description.as_deref(),
656 source_type.as_deref(),
657 source_location.as_deref(),
658 artifact_type.as_deref(),
659 artifact_location.as_ref().map(|loc| loc.as_deref()),
660 env_type.as_deref(),
661 env_image.as_deref(),
662 env_compute.as_deref(),
663 service_role.as_deref(),
664 tags,
665 account_id,
666 ) {
667 Ok(project) => wire::serialize_update_project_response(&wire::UpdateProjectOutput {
668 project: Some(project_to_wire(project)),
669 }),
670 Err(e) => codebuild_error_response(&e),
671 }
672 }
673
674 async fn handle_retry_build(
675 &self,
676 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
677 body: &[u8],
678 account_id: &str,
679 region: &str,
680 ) -> MockResponse {
681 let input = match wire::deserialize_retry_build_request(body) {
682 Ok(v) => v,
683 Err(e) => return json_error_response(400, "ValidationException", &e),
684 };
685 let build_id = match input.id.as_deref() {
686 Some(id) if !id.is_empty() => id,
687 _ => return json_error_response(400, "InvalidInputException", "id is required"),
688 };
689
690 let mut state = state.write().await;
691 match state.retry_build(build_id, account_id, region) {
692 Ok(build) => wire::serialize_retry_build_response(&wire::RetryBuildOutput {
693 build: Some(build_to_wire(build)),
694 }),
695 Err(e) => codebuild_error_response(&e),
696 }
697 }
698
699 async fn handle_create_webhook(
700 &self,
701 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
702 body: &[u8],
703 account_id: &str,
704 region: &str,
705 ) -> MockResponse {
706 let input = match wire::deserialize_create_webhook_request(body) {
707 Ok(v) => v,
708 Err(e) => return json_error_response(400, "ValidationException", &e),
709 };
710 if input.project_name.is_empty() {
711 return json_error_response(400, "InvalidInputException", "projectName is required");
712 }
713
714 let mut state = state.write().await;
715 match state.create_webhook(
716 &input.project_name,
717 input.branch_filter.as_deref(),
718 input.build_type.as_deref(),
719 account_id,
720 region,
721 ) {
722 Ok(wh) => wire::serialize_create_webhook_response(&wire::CreateWebhookOutput {
723 webhook: Some(webhook_to_wire(wh)),
724 }),
725 Err(e) => codebuild_error_response(&e),
726 }
727 }
728
729 async fn handle_update_webhook(
730 &self,
731 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
732 body: &[u8],
733 ) -> MockResponse {
734 let input = match wire::deserialize_update_webhook_request(body) {
735 Ok(v) => v,
736 Err(e) => return json_error_response(400, "ValidationException", &e),
737 };
738 if input.project_name.is_empty() {
739 return json_error_response(400, "InvalidInputException", "projectName is required");
740 }
741
742 let mut state = state.write().await;
743 match state.update_webhook(
744 &input.project_name,
745 input.branch_filter.as_deref(),
746 input.build_type.as_deref(),
747 ) {
748 Ok(wh) => wire::serialize_update_webhook_response(&wire::UpdateWebhookOutput {
749 webhook: Some(webhook_to_wire(wh)),
750 }),
751 Err(e) => codebuild_error_response(&e),
752 }
753 }
754
755 async fn handle_delete_webhook(
756 &self,
757 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
758 body: &[u8],
759 ) -> MockResponse {
760 let input = match wire::deserialize_delete_webhook_request(body) {
761 Ok(v) => v,
762 Err(e) => return json_error_response(400, "ValidationException", &e),
763 };
764 if input.project_name.is_empty() {
765 return json_error_response(400, "InvalidInputException", "projectName is required");
766 }
767
768 let mut state = state.write().await;
769 match state.delete_webhook(&input.project_name) {
770 Ok(()) => wire::serialize_delete_webhook_response(&wire::DeleteWebhookOutput {}),
771 Err(e) => codebuild_error_response(&e),
772 }
773 }
774
775 async fn handle_import_source_credentials(
776 &self,
777 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
778 body: &[u8],
779 account_id: &str,
780 region: &str,
781 ) -> MockResponse {
782 let input = match wire::deserialize_import_source_credentials_request(body) {
783 Ok(v) => v,
784 Err(e) => return json_error_response(400, "ValidationException", &e),
785 };
786 if input.token.is_empty() {
787 return json_error_response(400, "InvalidInputException", "token is required");
788 }
789 if input.server_type.is_empty() {
790 return json_error_response(400, "InvalidInputException", "serverType is required");
791 }
792 if input.auth_type.is_empty() {
793 return json_error_response(400, "InvalidInputException", "authType is required");
794 }
795
796 let mut state = state.write().await;
797 match state.import_source_credentials(
798 &input.token,
799 &input.server_type,
800 &input.auth_type,
801 input.username.as_deref(),
802 account_id,
803 region,
804 ) {
805 Ok(cred) => wire::serialize_import_source_credentials_response(
806 &wire::ImportSourceCredentialsOutput {
807 arn: Some(cred.arn.clone()),
808 },
809 ),
810 Err(e) => codebuild_error_response(&e),
811 }
812 }
813
814 async fn handle_list_source_credentials(
815 &self,
816 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
817 ) -> MockResponse {
818 let state = state.read().await;
819 let creds: Vec<wire::SourceCredentialsInfo> = state
820 .list_source_credentials()
821 .iter()
822 .map(|c| wire::SourceCredentialsInfo {
823 arn: Some(c.arn.clone()),
824 server_type: Some(c.server_type.clone()),
825 auth_type: Some(c.auth_type.clone()),
826 resource: c.resource.clone(),
827 })
828 .collect();
829
830 wire::serialize_list_source_credentials_response(&wire::ListSourceCredentialsOutput {
831 source_credentials_infos: Some(creds),
832 })
833 }
834
835 async fn handle_delete_source_credentials(
836 &self,
837 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
838 body: &[u8],
839 ) -> MockResponse {
840 let input = match wire::deserialize_delete_source_credentials_request(body) {
841 Ok(v) => v,
842 Err(e) => return json_error_response(400, "ValidationException", &e),
843 };
844 if input.arn.is_empty() {
845 return json_error_response(400, "InvalidInputException", "arn is required");
846 }
847
848 let mut state = state.write().await;
849 match state.delete_source_credentials(&input.arn) {
850 Ok(()) => wire::serialize_delete_source_credentials_response(
851 &wire::DeleteSourceCredentialsOutput {
852 arn: Some(input.arn.clone()),
853 },
854 ),
855 Err(e) => codebuild_error_response(&e),
856 }
857 }
858
859 async fn handle_put_resource_policy(
860 &self,
861 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
862 body: &[u8],
863 ) -> MockResponse {
864 let input = match wire::deserialize_put_resource_policy_request(body) {
865 Ok(v) => v,
866 Err(e) => return json_error_response(400, "ValidationException", &e),
867 };
868 if input.resource_arn.is_empty() {
869 return json_error_response(400, "InvalidInputException", "resourceArn is required");
870 }
871 if input.policy.is_empty() {
872 return json_error_response(400, "InvalidInputException", "policy is required");
873 }
874
875 let mut state = state.write().await;
876 match state.put_resource_policy(&input.resource_arn, &input.policy) {
877 Ok(arn) => {
878 wire::serialize_put_resource_policy_response(&wire::PutResourcePolicyOutput {
879 resource_arn: Some(arn.to_string()),
880 })
881 }
882 Err(e) => codebuild_error_response(&e),
883 }
884 }
885
886 async fn handle_get_resource_policy(
887 &self,
888 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
889 body: &[u8],
890 ) -> MockResponse {
891 let input = match wire::deserialize_get_resource_policy_request(body) {
892 Ok(v) => v,
893 Err(e) => return json_error_response(400, "ValidationException", &e),
894 };
895 if input.resource_arn.is_empty() {
896 return json_error_response(400, "InvalidInputException", "resourceArn is required");
897 }
898
899 let state = state.read().await;
900 match state.get_resource_policy(&input.resource_arn) {
901 Ok(policy) => {
902 wire::serialize_get_resource_policy_response(&wire::GetResourcePolicyOutput {
903 policy: Some(policy),
904 })
905 }
906 Err(e) => codebuild_error_response(&e),
907 }
908 }
909
910 async fn handle_delete_resource_policy(
911 &self,
912 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
913 body: &[u8],
914 ) -> MockResponse {
915 let input = match wire::deserialize_delete_resource_policy_request(body) {
916 Ok(v) => v,
917 Err(e) => return json_error_response(400, "ValidationException", &e),
918 };
919 if input.resource_arn.is_empty() {
920 return json_error_response(400, "InvalidInputException", "resourceArn is required");
921 }
922
923 let mut state = state.write().await;
924 match state.delete_resource_policy(&input.resource_arn) {
925 Ok(()) => wire::serialize_delete_resource_policy_response(
926 &wire::DeleteResourcePolicyOutput {},
927 ),
928 Err(e) => codebuild_error_response(&e),
929 }
930 }
931
932 async fn handle_invalidate_project_cache(
933 &self,
934 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
935 body: &[u8],
936 ) -> MockResponse {
937 let input = match wire::deserialize_invalidate_project_cache_request(body) {
938 Ok(v) => v,
939 Err(e) => return json_error_response(400, "ValidationException", &e),
940 };
941 if !input.project_name.is_empty() {
943 let s = state.read().await;
944 if !s.projects.contains_key(&input.project_name) {
945 let project_name = input.project_name;
946 return json_error_response(
947 400,
948 "ResourceNotFoundException",
949 &format!("Project {project_name} not found"),
950 );
951 }
952 }
953 wire::serialize_invalidate_project_cache_response(&wire::InvalidateProjectCacheOutput {})
954 }
955
956 async fn handle_create_report_group(
959 &self,
960 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
961 body: &[u8],
962 account_id: &str,
963 region: &str,
964 ) -> MockResponse {
965 let input = match wire::deserialize_create_report_group_request(body) {
966 Ok(v) => v,
967 Err(e) => return json_error_response(400, "ValidationException", &e),
968 };
969 if input.name.is_empty() {
970 return json_error_response(400, "InvalidInputException", "name is required");
971 }
972 let report_type = if input.r#type.is_empty() {
973 "TEST"
974 } else {
975 input.r#type.as_str()
976 };
977 let export_config_type = input.export_config.export_config_type.as_deref();
978 let tags = tags_from_wire(input.tags.unwrap_or_default());
979
980 let mut state = state.write().await;
981 match state.create_report_group(
982 &input.name,
983 report_type,
984 export_config_type,
985 tags,
986 account_id,
987 region,
988 ) {
989 Ok(rg) => {
990 wire::serialize_create_report_group_response(&wire::CreateReportGroupOutput {
991 report_group: Some(report_group_to_wire(rg)),
992 })
993 }
994 Err(e) => codebuild_error_response(&e),
995 }
996 }
997
998 async fn handle_batch_get_report_groups(
999 &self,
1000 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
1001 body: &[u8],
1002 ) -> MockResponse {
1003 let input = match wire::deserialize_batch_get_report_groups_request(body) {
1004 Ok(v) => v,
1005 Err(e) => return json_error_response(400, "ValidationException", &e),
1006 };
1007 let arns = input.report_group_arns;
1008
1009 let state = state.read().await;
1010 let found = state.batch_get_report_groups(&arns);
1011 let found_arns: Vec<&str> = found.iter().map(|rg| rg.arn.as_str()).collect();
1012 let not_found: Vec<String> = arns
1013 .iter()
1014 .filter(|arn| !found_arns.contains(&arn.as_str()))
1015 .cloned()
1016 .collect();
1017
1018 wire::serialize_batch_get_report_groups_response(&wire::BatchGetReportGroupsOutput {
1019 report_groups: Some(found.iter().map(|rg| report_group_to_wire(rg)).collect()),
1020 report_groups_not_found: Some(not_found),
1021 })
1022 }
1023
1024 async fn handle_list_report_groups(
1025 &self,
1026 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
1027 ) -> MockResponse {
1028 let state = state.read().await;
1029 let arns: Vec<String> = state
1030 .list_report_groups()
1031 .into_iter()
1032 .map(|s| s.to_string())
1033 .collect();
1034
1035 wire::serialize_list_report_groups_response(&wire::ListReportGroupsOutput {
1036 report_groups: Some(arns),
1037 next_token: None,
1038 })
1039 }
1040
1041 async fn handle_delete_report_group(
1042 &self,
1043 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
1044 body: &[u8],
1045 ) -> MockResponse {
1046 let input = match wire::deserialize_delete_report_group_request(body) {
1047 Ok(v) => v,
1048 Err(e) => return json_error_response(400, "ValidationException", &e),
1049 };
1050 if input.arn.is_empty() {
1051 return json_error_response(400, "InvalidInputException", "arn is required");
1052 }
1053
1054 let mut state = state.write().await;
1055 match state.delete_report_group(&input.arn) {
1056 Ok(()) => {
1057 wire::serialize_delete_report_group_response(&wire::DeleteReportGroupOutput {})
1058 }
1059 Err(e) => codebuild_error_response(&e),
1060 }
1061 }
1062
1063 async fn handle_update_report_group(
1064 &self,
1065 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
1066 body: &[u8],
1067 ) -> MockResponse {
1068 let input = match wire::deserialize_update_report_group_request(body) {
1069 Ok(v) => v,
1070 Err(e) => return json_error_response(400, "ValidationException", &e),
1071 };
1072 if input.arn.is_empty() {
1073 return json_error_response(400, "InvalidInputException", "arn is required");
1074 }
1075 let export_config_type = input
1076 .export_config
1077 .as_ref()
1078 .and_then(|c| c.export_config_type.as_deref());
1079 let tags = input.tags.map(tags_from_wire);
1080
1081 let mut state = state.write().await;
1082 match state.update_report_group(&input.arn, export_config_type, tags) {
1083 Ok(rg) => {
1084 wire::serialize_update_report_group_response(&wire::UpdateReportGroupOutput {
1085 report_group: Some(report_group_to_wire(rg)),
1086 })
1087 }
1088 Err(e) => codebuild_error_response(&e),
1089 }
1090 }
1091
1092 async fn handle_list_reports_for_report_group(
1093 &self,
1094 state: &Arc<tokio::sync::RwLock<CodeBuildState>>,
1095 body: &[u8],
1096 ) -> MockResponse {
1097 let input = match wire::deserialize_list_reports_for_report_group_request(body) {
1098 Ok(v) => v,
1099 Err(e) => return json_error_response(400, "ValidationException", &e),
1100 };
1101 if input.report_group_arn.is_empty() {
1102 return json_error_response(400, "InvalidInputException", "reportGroupArn is required");
1103 }
1104
1105 let state = state.read().await;
1106 if !state.report_groups.contains_key(&input.report_group_arn) {
1107 let arn = input.report_group_arn;
1108 return json_error_response(
1109 400,
1110 "ResourceNotFoundException",
1111 &format!("Report group {arn} does not exist"),
1112 );
1113 }
1114
1115 wire::serialize_list_reports_for_report_group_response(
1116 &wire::ListReportsForReportGroupOutput {
1117 reports: Some(vec![]),
1118 next_token: None,
1119 },
1120 )
1121 }
1122}
1123
1124fn tags_from_wire(tags: Vec<wire::Tag>) -> Vec<crate::types::Tag> {
1125 tags.into_iter()
1126 .map(|t| crate::types::Tag {
1127 key: t.key.unwrap_or_default(),
1128 value: t.value.unwrap_or_default(),
1129 })
1130 .collect()
1131}
1132
1133fn report_group_to_wire(rg: &crate::types::ReportGroup) -> wire::ReportGroup {
1134 let tags: Option<Vec<wire::Tag>> = if rg.tags.is_empty() {
1135 None
1136 } else {
1137 Some(
1138 rg.tags
1139 .iter()
1140 .map(|t| wire::Tag {
1141 key: Some(t.key.clone()),
1142 value: Some(t.value.clone()),
1143 })
1144 .collect(),
1145 )
1146 };
1147
1148 wire::ReportGroup {
1149 arn: Some(rg.arn.clone()),
1150 name: Some(rg.name.clone()),
1151 r#type: Some(rg.r#type.clone()),
1152 status: Some(rg.status.clone()),
1153 created: Some(rg.created.timestamp() as f64),
1154 last_modified: Some(rg.last_modified.timestamp() as f64),
1155 tags,
1156 export_config: rg
1157 .export_config_type
1158 .as_ref()
1159 .map(|ect| wire::ReportExportConfig {
1160 export_config_type: Some(ect.clone()),
1161 ..Default::default()
1162 }),
1163 }
1164}
1165
1166fn webhook_to_wire(wh: &crate::types::Webhook) -> wire::Webhook {
1167 wire::Webhook {
1168 url: Some(wh.url.clone()),
1169 branch_filter: wh.branch_filter.clone(),
1170 build_type: wh.build_type.clone(),
1171 secret: wh.secret.clone(),
1172 ..Default::default()
1173 }
1174}
1175
1176fn project_to_wire(p: &crate::types::Project) -> wire::Project {
1177 let tags: Option<Vec<wire::Tag>> = if p.tags.is_empty() {
1178 None
1179 } else {
1180 Some(
1181 p.tags
1182 .iter()
1183 .map(|t| wire::Tag {
1184 key: Some(t.key.clone()),
1185 value: Some(t.value.clone()),
1186 })
1187 .collect(),
1188 )
1189 };
1190
1191 let artifacts = Some(wire::ProjectArtifacts {
1192 r#type: p.artifact_type.clone(),
1193 location: p.artifact_location.clone(),
1194 ..Default::default()
1195 });
1196
1197 wire::Project {
1198 name: Some(p.name.clone()),
1199 arn: Some(p.arn.clone()),
1200 description: if p.description.is_empty() {
1201 None
1202 } else {
1203 Some(p.description.clone())
1204 },
1205 source: Some(wire::ProjectSource {
1206 r#type: p.source_type.clone(),
1207 location: if p.source_location.is_empty() {
1208 None
1209 } else {
1210 Some(p.source_location.clone())
1211 },
1212 ..Default::default()
1213 }),
1214 artifacts,
1215 environment: Some(wire::ProjectEnvironment {
1216 r#type: p.environment_type.clone(),
1217 image: p.environment_image.clone(),
1218 compute_type: p.environment_compute_type.clone(),
1219 ..Default::default()
1220 }),
1221 service_role: Some(p.service_role.clone()),
1222 tags,
1223 created: Some(p.created.timestamp() as f64),
1224 last_modified: Some(p.last_modified.timestamp() as f64),
1225 ..Default::default()
1226 }
1227}
1228
1229fn build_to_wire(b: &crate::types::Build) -> wire::Build {
1230 let phases: Vec<wire::BuildPhase> = b
1231 .phases
1232 .iter()
1233 .map(|p| wire::BuildPhase {
1234 phase_type: Some(p.phase_type.clone()),
1235 phase_status: p.phase_status.clone(),
1236 start_time: Some(p.start_time),
1237 end_time: p.end_time,
1238 duration_in_seconds: p.duration_in_seconds,
1239 ..Default::default()
1240 })
1241 .collect();
1242
1243 wire::Build {
1244 id: Some(b.id.clone()),
1245 arn: Some(b.arn.clone()),
1246 project_name: Some(b.project_name.clone()),
1247 build_status: Some(b.build_status.clone()),
1248 current_phase: Some(b.current_phase.clone()),
1249 source_version: Some(b.source_version.clone()),
1250 source: Some(wire::ProjectSource {
1251 r#type: b.source_type.clone(),
1252 location: if b.source_location.is_empty() {
1253 None
1254 } else {
1255 Some(b.source_location.clone())
1256 },
1257 ..Default::default()
1258 }),
1259 artifacts: if b.artifact_type == "NO_ARTIFACTS" {
1260 None
1261 } else {
1262 Some(wire::BuildArtifacts {
1263 location: b.artifact_location.clone(),
1264 ..Default::default()
1265 })
1266 },
1267 environment: Some(wire::ProjectEnvironment {
1268 r#type: b.environment_type.clone(),
1269 image: b.environment_image.clone(),
1270 compute_type: b.environment_compute_type.clone(),
1271 ..Default::default()
1272 }),
1273 service_role: Some(b.service_role.clone()),
1274 start_time: Some(b.start_time.timestamp() as f64),
1275 end_time: b.end_time.map(|t| t.timestamp() as f64),
1276 build_number: Some(b.build_number),
1277 phases: Some(phases),
1278 ..Default::default()
1279 }
1280}
1281
1282fn codebuild_error_response(err: &CodeBuildError) -> MockResponse {
1283 use CodeBuildError::*;
1284 let (status, error_type) = match err {
1285 InvalidProjectName => (400, "InvalidInputException"),
1286 InvalidServiceRole => (400, "InvalidInputException"),
1287 InvalidBuildId => (400, "InvalidInputException"),
1288 ProjectAlreadyExists { .. } => (400, "ResourceAlreadyExistsException"),
1289 ProjectNotFound { .. } => (400, "ResourceNotFoundException"),
1290 BuildNotFound { .. } => (400, "ResourceNotFoundException"),
1291 ProjectDoesNotExist { .. } => (400, "ResourceNotFoundException"),
1292 WebhookAlreadyExists { .. } => (400, "ResourceAlreadyExistsException"),
1293 WebhookNotFound { .. } => (400, "ResourceNotFoundException"),
1294 SourceCredentialsNotFound { .. } => (400, "ResourceNotFoundException"),
1295 ResourcePolicyNotFound { .. } => (400, "ResourceNotFoundException"),
1296 ReportGroupAlreadyExists { .. } => (400, "ResourceAlreadyExistsException"),
1297 ReportGroupNotFound { .. } => (400, "ResourceNotFoundException"),
1298 };
1299 MockResponse::json(
1300 status,
1301 json!({"__type": error_type, "message": err.to_string()}).to_string(),
1302 )
1303}