1use crate::error::{ValidationError, ValidationErrorKind};
6use crate::types::{DeploymentSpec, EndpointSpec, ResourceType, ScaleSpec, ServiceSpec};
7use cron::Schedule;
8use std::collections::HashSet;
9use std::str::FromStr;
10
11fn make_validation_error(
18 code: &'static str,
19 message: impl Into<std::borrow::Cow<'static, str>>,
20) -> validator::ValidationError {
21 let mut err = validator::ValidationError::new(code);
22 err.message = Some(message.into());
23 err
24}
25
26pub fn validate_version_wrapper(version: &str) -> Result<(), validator::ValidationError> {
28 if version == "v1" {
29 Ok(())
30 } else {
31 Err(make_validation_error(
32 "invalid_version",
33 format!("version must be 'v1', found '{}'", version),
34 ))
35 }
36}
37
38pub fn validate_deployment_name_wrapper(name: &str) -> Result<(), validator::ValidationError> {
40 if name.len() < 3 || name.len() > 63 {
42 return Err(make_validation_error(
43 "invalid_deployment_name",
44 "deployment name must be 3-63 characters",
45 ));
46 }
47
48 if let Some(first) = name.chars().next() {
50 if !first.is_ascii_alphanumeric() {
51 return Err(make_validation_error(
52 "invalid_deployment_name",
53 "deployment name must start with alphanumeric character",
54 ));
55 }
56 }
57
58 for c in name.chars() {
60 if !c.is_ascii_alphanumeric() && c != '-' {
61 return Err(make_validation_error(
62 "invalid_deployment_name",
63 "deployment name can only contain alphanumeric characters and hyphens",
64 ));
65 }
66 }
67
68 Ok(())
69}
70
71pub fn validate_image_name_wrapper(name: &str) -> Result<(), validator::ValidationError> {
73 if name.is_empty() || name.trim().is_empty() {
74 Err(make_validation_error(
75 "empty_image_name",
76 "image name cannot be empty",
77 ))
78 } else {
79 Ok(())
80 }
81}
82
83pub fn validate_cpu_option_wrapper(cpu: f64) -> Result<(), validator::ValidationError> {
86 if cpu <= 0.0 {
87 Err(make_validation_error(
88 "invalid_cpu",
89 format!("CPU limit must be > 0, found {}", cpu),
90 ))
91 } else {
92 Ok(())
93 }
94}
95
96pub fn validate_memory_option_wrapper(value: &String) -> Result<(), validator::ValidationError> {
99 const VALID_SUFFIXES: [&str; 4] = ["Ki", "Mi", "Gi", "Ti"];
100
101 let suffix_match = VALID_SUFFIXES
102 .iter()
103 .find(|&&suffix| value.ends_with(suffix));
104
105 match suffix_match {
106 Some(suffix) => {
107 let numeric_part = &value[..value.len() - suffix.len()];
108 match numeric_part.parse::<u64>() {
109 Ok(n) if n > 0 => Ok(()),
110 _ => Err(make_validation_error(
111 "invalid_memory_format",
112 format!("invalid memory format: '{}'", value),
113 )),
114 }
115 }
116 None => Err(make_validation_error(
117 "invalid_memory_format",
118 format!(
119 "invalid memory format: '{}' (use Ki, Mi, Gi, or Ti suffix)",
120 value
121 ),
122 )),
123 }
124}
125
126pub fn validate_port_wrapper(port: u16) -> Result<(), validator::ValidationError> {
129 if port >= 1 {
130 Ok(())
131 } else {
132 Err(make_validation_error(
133 "invalid_port",
134 "port must be between 1-65535",
135 ))
136 }
137}
138
139pub fn validate_scale_spec(scale: &ScaleSpec) -> Result<(), validator::ValidationError> {
141 if let ScaleSpec::Adaptive { min, max, .. } = scale {
142 if *min > *max {
143 return Err(make_validation_error(
144 "invalid_scale_range",
145 format!("scale min ({}) cannot be greater than max ({})", min, max),
146 ));
147 }
148 }
149 Ok(())
150}
151
152pub fn validate_schedule_wrapper(schedule: &String) -> Result<(), validator::ValidationError> {
155 Schedule::from_str(schedule).map(|_| ()).map_err(|e| {
156 make_validation_error(
157 "invalid_cron_schedule",
158 format!("invalid cron schedule '{}': {}", schedule, e),
159 )
160 })
161}
162
163pub fn validate_secret_reference(value: &str) -> Result<(), validator::ValidationError> {
175 if !value.starts_with("$S:") {
177 return Ok(());
178 }
179
180 let secret_ref = &value[3..]; if secret_ref.is_empty() {
183 return Err(make_validation_error(
184 "invalid_secret_reference",
185 "secret reference cannot be empty after $S:",
186 ));
187 }
188
189 let secret_name = if let Some(rest) = secret_ref.strip_prefix('@') {
191 let parts: Vec<&str> = rest.splitn(2, '/').collect();
193 if parts.len() != 2 {
194 return Err(make_validation_error(
195 "invalid_secret_reference",
196 format!(
197 "cross-service secret reference '{}' must have format @service/secret-name",
198 value
199 ),
200 ));
201 }
202
203 let service_name = parts[0];
204 let secret_name = parts[1];
205
206 if service_name.is_empty() {
208 return Err(make_validation_error(
209 "invalid_secret_reference",
210 format!(
211 "service name in secret reference '{}' cannot be empty",
212 value
213 ),
214 ));
215 }
216
217 if !service_name.chars().next().unwrap().is_ascii_alphabetic() {
218 return Err(make_validation_error(
219 "invalid_secret_reference",
220 format!(
221 "service name in secret reference '{}' must start with a letter",
222 value
223 ),
224 ));
225 }
226
227 for c in service_name.chars() {
228 if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
229 return Err(make_validation_error(
230 "invalid_secret_reference",
231 format!(
232 "service name in secret reference '{}' contains invalid character '{}'",
233 value, c
234 ),
235 ));
236 }
237 }
238
239 secret_name
240 } else {
241 secret_ref
242 };
243
244 if secret_name.is_empty() {
246 return Err(make_validation_error(
247 "invalid_secret_reference",
248 format!("secret name in '{}' cannot be empty", value),
249 ));
250 }
251
252 let first_char = secret_name.chars().next().unwrap();
254 if !first_char.is_ascii_alphabetic() {
255 return Err(make_validation_error(
256 "invalid_secret_reference",
257 format!(
258 "secret name in '{}' must start with a letter, found '{}'",
259 value, first_char
260 ),
261 ));
262 }
263
264 for c in secret_name.chars() {
266 if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
267 return Err(make_validation_error(
268 "invalid_secret_reference",
269 format!(
270 "secret name in '{}' contains invalid character '{}' (only alphanumeric, hyphens, underscores allowed)",
271 value, c
272 ),
273 ));
274 }
275 }
276
277 Ok(())
278}
279
280pub fn validate_env_vars(
282 service_name: &str,
283 env: &std::collections::HashMap<String, String>,
284) -> Result<(), crate::error::ValidationError> {
285 for (key, value) in env {
286 if let Err(e) = validate_secret_reference(value) {
287 return Err(crate::error::ValidationError {
288 kind: crate::error::ValidationErrorKind::InvalidEnvVar {
289 key: key.clone(),
290 reason: e
291 .message
292 .map(|m| m.to_string())
293 .unwrap_or_else(|| "invalid secret reference".to_string()),
294 },
295 path: format!("services.{}.env.{}", service_name, key),
296 });
297 }
298 }
299 Ok(())
300}
301
302pub fn validate_storage_name(name: &str) -> Result<(), validator::ValidationError> {
304 let re = regex::Regex::new(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$").unwrap();
306 if !re.is_match(name) || name.len() > 63 {
307 return Err(make_validation_error(
308 "invalid_storage_name",
309 format!("storage name '{}' must be lowercase alphanumeric with hyphens, 1-63 chars, not starting/ending with hyphen", name),
310 ));
311 }
312 Ok(())
313}
314
315pub fn validate_storage_name_wrapper(name: &str) -> Result<(), validator::ValidationError> {
317 validate_storage_name(name)
318}
319
320pub fn validate_dependencies(spec: &DeploymentSpec) -> Result<(), ValidationError> {
326 let service_names: HashSet<&str> = spec.services.keys().map(|s| s.as_str()).collect();
327
328 for (service_name, service_spec) in &spec.services {
329 for dep in &service_spec.depends {
330 if !service_names.contains(dep.service.as_str()) {
331 return Err(ValidationError {
332 kind: ValidationErrorKind::UnknownDependency {
333 service: dep.service.clone(),
334 },
335 path: format!("services.{}.depends", service_name),
336 });
337 }
338 }
339 }
340
341 Ok(())
342}
343
344pub fn validate_unique_service_endpoints(spec: &DeploymentSpec) -> Result<(), ValidationError> {
346 for (service_name, service_spec) in &spec.services {
347 let mut seen = HashSet::new();
348 for endpoint in &service_spec.endpoints {
349 if !seen.insert(&endpoint.name) {
350 return Err(ValidationError {
351 kind: ValidationErrorKind::DuplicateEndpoint {
352 name: endpoint.name.clone(),
353 },
354 path: format!("services.{}.endpoints", service_name),
355 });
356 }
357 }
358 }
359
360 Ok(())
361}
362
363pub fn validate_cron_schedules(spec: &DeploymentSpec) -> Result<(), ValidationError> {
365 for (service_name, service_spec) in &spec.services {
366 validate_service_schedule(service_name, service_spec)?;
367 }
368 Ok(())
369}
370
371pub fn validate_service_schedule(
373 service_name: &str,
374 spec: &ServiceSpec,
375) -> Result<(), ValidationError> {
376 if spec.schedule.is_some() && spec.rtype != ResourceType::Cron {
378 return Err(ValidationError {
379 kind: ValidationErrorKind::ScheduleOnlyForCron,
380 path: format!("services.{}.schedule", service_name),
381 });
382 }
383
384 if spec.rtype == ResourceType::Cron && spec.schedule.is_none() {
386 return Err(ValidationError {
387 kind: ValidationErrorKind::CronRequiresSchedule,
388 path: format!("services.{}.schedule", service_name),
389 });
390 }
391
392 Ok(())
393}
394
395pub fn validate_version(version: &str) -> Result<(), ValidationError> {
401 if version == "v1" {
402 Ok(())
403 } else {
404 Err(ValidationError {
405 kind: ValidationErrorKind::InvalidVersion {
406 found: version.to_string(),
407 },
408 path: "version".to_string(),
409 })
410 }
411}
412
413pub fn validate_deployment_name(name: &str) -> Result<(), ValidationError> {
420 if name.len() < 3 || name.len() > 63 {
422 return Err(ValidationError {
423 kind: ValidationErrorKind::EmptyDeploymentName,
424 path: "deployment".to_string(),
425 });
426 }
427
428 if let Some(first) = name.chars().next() {
430 if !first.is_ascii_alphanumeric() {
431 return Err(ValidationError {
432 kind: ValidationErrorKind::EmptyDeploymentName,
433 path: "deployment".to_string(),
434 });
435 }
436 }
437
438 for c in name.chars() {
440 if !c.is_ascii_alphanumeric() && c != '-' {
441 return Err(ValidationError {
442 kind: ValidationErrorKind::EmptyDeploymentName,
443 path: "deployment".to_string(),
444 });
445 }
446 }
447
448 Ok(())
449}
450
451pub fn validate_image_name(name: &str) -> Result<(), ValidationError> {
457 if name.is_empty() || name.trim().is_empty() {
458 Err(ValidationError {
459 kind: ValidationErrorKind::EmptyImageName,
460 path: "image.name".to_string(),
461 })
462 } else {
463 Ok(())
464 }
465}
466
467pub fn validate_cpu(cpu: &f64) -> Result<(), ValidationError> {
472 if *cpu > 0.0 {
473 Ok(())
474 } else {
475 Err(ValidationError {
476 kind: ValidationErrorKind::InvalidCpu { cpu: *cpu },
477 path: "resources.cpu".to_string(),
478 })
479 }
480}
481
482pub fn validate_memory_format(value: &str) -> Result<(), ValidationError> {
487 const VALID_SUFFIXES: [&str; 4] = ["Ki", "Mi", "Gi", "Ti"];
489
490 let suffix_match = VALID_SUFFIXES
492 .iter()
493 .find(|&&suffix| value.ends_with(suffix));
494
495 match suffix_match {
496 Some(suffix) => {
497 let numeric_part = &value[..value.len() - suffix.len()];
499
500 match numeric_part.parse::<u64>() {
502 Ok(n) if n > 0 => Ok(()),
503 _ => Err(ValidationError {
504 kind: ValidationErrorKind::InvalidMemoryFormat {
505 value: value.to_string(),
506 },
507 path: "resources.memory".to_string(),
508 }),
509 }
510 }
511 None => Err(ValidationError {
512 kind: ValidationErrorKind::InvalidMemoryFormat {
513 value: value.to_string(),
514 },
515 path: "resources.memory".to_string(),
516 }),
517 }
518}
519
520pub fn validate_port(port: &u16) -> Result<(), ValidationError> {
525 if *port >= 1 {
526 Ok(())
527 } else {
528 Err(ValidationError {
529 kind: ValidationErrorKind::InvalidPort { port: *port as u32 },
530 path: "endpoints[].port".to_string(),
531 })
532 }
533}
534
535pub fn validate_unique_endpoints(endpoints: &[EndpointSpec]) -> Result<(), ValidationError> {
537 let mut seen = HashSet::new();
538
539 for endpoint in endpoints {
540 if !seen.insert(&endpoint.name) {
541 return Err(ValidationError {
542 kind: ValidationErrorKind::DuplicateEndpoint {
543 name: endpoint.name.clone(),
544 },
545 path: "endpoints".to_string(),
546 });
547 }
548 }
549
550 Ok(())
551}
552
553pub fn validate_scale_range(min: u32, max: u32) -> Result<(), ValidationError> {
558 if min <= max {
559 Ok(())
560 } else {
561 Err(ValidationError {
562 kind: ValidationErrorKind::InvalidScaleRange { min, max },
563 path: "scale".to_string(),
564 })
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::types::{ExposeType, Protocol};
572
573 #[test]
575 fn test_validate_version_valid() {
576 assert!(validate_version("v1").is_ok());
577 }
578
579 #[test]
580 fn test_validate_version_invalid_v2() {
581 let result = validate_version("v2");
582 assert!(result.is_err());
583 let err = result.unwrap_err();
584 assert!(matches!(
585 err.kind,
586 ValidationErrorKind::InvalidVersion { found } if found == "v2"
587 ));
588 }
589
590 #[test]
591 fn test_validate_version_empty() {
592 let result = validate_version("");
593 assert!(result.is_err());
594 let err = result.unwrap_err();
595 assert!(matches!(
596 err.kind,
597 ValidationErrorKind::InvalidVersion { found } if found.is_empty()
598 ));
599 }
600
601 #[test]
603 fn test_validate_deployment_name_valid() {
604 assert!(validate_deployment_name("my-app").is_ok());
605 assert!(validate_deployment_name("api").is_ok());
606 assert!(validate_deployment_name("my-service-123").is_ok());
607 assert!(validate_deployment_name("a1b").is_ok());
608 }
609
610 #[test]
611 fn test_validate_deployment_name_too_short() {
612 assert!(validate_deployment_name("ab").is_err());
613 assert!(validate_deployment_name("a").is_err());
614 assert!(validate_deployment_name("").is_err());
615 }
616
617 #[test]
618 fn test_validate_deployment_name_too_long() {
619 let long_name = "a".repeat(64);
620 assert!(validate_deployment_name(&long_name).is_err());
621 }
622
623 #[test]
624 fn test_validate_deployment_name_invalid_chars() {
625 assert!(validate_deployment_name("my_app").is_err()); assert!(validate_deployment_name("my.app").is_err()); assert!(validate_deployment_name("my app").is_err()); assert!(validate_deployment_name("my@app").is_err()); }
630
631 #[test]
632 fn test_validate_deployment_name_must_start_alphanumeric() {
633 assert!(validate_deployment_name("-myapp").is_err());
634 assert!(validate_deployment_name("_myapp").is_err());
635 }
636
637 #[test]
639 fn test_validate_image_name_valid() {
640 assert!(validate_image_name("nginx:latest").is_ok());
641 assert!(validate_image_name("ghcr.io/org/api:v1.2.3").is_ok());
642 assert!(validate_image_name("ubuntu").is_ok());
643 }
644
645 #[test]
646 fn test_validate_image_name_empty() {
647 let result = validate_image_name("");
648 assert!(result.is_err());
649 assert!(matches!(
650 result.unwrap_err().kind,
651 ValidationErrorKind::EmptyImageName
652 ));
653 }
654
655 #[test]
656 fn test_validate_image_name_whitespace_only() {
657 assert!(validate_image_name(" ").is_err());
658 assert!(validate_image_name("\t\n").is_err());
659 }
660
661 #[test]
663 fn test_validate_cpu_valid() {
664 assert!(validate_cpu(&0.5).is_ok());
665 assert!(validate_cpu(&1.0).is_ok());
666 assert!(validate_cpu(&2.0).is_ok());
667 assert!(validate_cpu(&0.001).is_ok());
668 }
669
670 #[test]
671 fn test_validate_cpu_zero() {
672 let result = validate_cpu(&0.0);
673 assert!(result.is_err());
674 assert!(matches!(
675 result.unwrap_err().kind,
676 ValidationErrorKind::InvalidCpu { cpu } if cpu == 0.0
677 ));
678 }
679
680 #[test]
681 fn test_validate_cpu_negative() {
682 let result = validate_cpu(&-1.0);
683 assert!(result.is_err());
684 assert!(matches!(
685 result.unwrap_err().kind,
686 ValidationErrorKind::InvalidCpu { cpu } if cpu == -1.0
687 ));
688 }
689
690 #[test]
692 fn test_validate_memory_format_valid() {
693 assert!(validate_memory_format("512Mi").is_ok());
694 assert!(validate_memory_format("1Gi").is_ok());
695 assert!(validate_memory_format("2Ti").is_ok());
696 assert!(validate_memory_format("256Ki").is_ok());
697 assert!(validate_memory_format("4096Mi").is_ok());
698 }
699
700 #[test]
701 fn test_validate_memory_format_invalid_suffix() {
702 assert!(validate_memory_format("512MB").is_err());
703 assert!(validate_memory_format("1GB").is_err());
704 assert!(validate_memory_format("512").is_err());
705 assert!(validate_memory_format("512m").is_err());
706 }
707
708 #[test]
709 fn test_validate_memory_format_no_number() {
710 assert!(validate_memory_format("Mi").is_err());
711 assert!(validate_memory_format("Gi").is_err());
712 }
713
714 #[test]
715 fn test_validate_memory_format_invalid_number() {
716 assert!(validate_memory_format("-512Mi").is_err());
717 assert!(validate_memory_format("0Mi").is_err());
718 assert!(validate_memory_format("abcMi").is_err());
719 }
720
721 #[test]
723 fn test_validate_port_valid() {
724 assert!(validate_port(&1).is_ok());
725 assert!(validate_port(&80).is_ok());
726 assert!(validate_port(&443).is_ok());
727 assert!(validate_port(&8080).is_ok());
728 assert!(validate_port(&65535).is_ok());
729 }
730
731 #[test]
732 fn test_validate_port_zero() {
733 let result = validate_port(&0);
734 assert!(result.is_err());
735 assert!(matches!(
736 result.unwrap_err().kind,
737 ValidationErrorKind::InvalidPort { port } if port == 0
738 ));
739 }
740
741 #[test]
746 fn test_validate_unique_endpoints_valid() {
747 let endpoints = vec![
748 EndpointSpec {
749 name: "http".to_string(),
750 protocol: Protocol::Http,
751 port: 8080,
752 path: None,
753 expose: ExposeType::Public,
754 },
755 EndpointSpec {
756 name: "grpc".to_string(),
757 protocol: Protocol::Tcp,
758 port: 9090,
759 path: None,
760 expose: ExposeType::Internal,
761 },
762 ];
763 assert!(validate_unique_endpoints(&endpoints).is_ok());
764 }
765
766 #[test]
767 fn test_validate_unique_endpoints_empty() {
768 let endpoints: Vec<EndpointSpec> = vec![];
769 assert!(validate_unique_endpoints(&endpoints).is_ok());
770 }
771
772 #[test]
773 fn test_validate_unique_endpoints_duplicates() {
774 let endpoints = vec![
775 EndpointSpec {
776 name: "http".to_string(),
777 protocol: Protocol::Http,
778 port: 8080,
779 path: None,
780 expose: ExposeType::Public,
781 },
782 EndpointSpec {
783 name: "http".to_string(), protocol: Protocol::Https,
785 port: 8443,
786 path: None,
787 expose: ExposeType::Public,
788 },
789 ];
790 let result = validate_unique_endpoints(&endpoints);
791 assert!(result.is_err());
792 assert!(matches!(
793 result.unwrap_err().kind,
794 ValidationErrorKind::DuplicateEndpoint { name } if name == "http"
795 ));
796 }
797
798 #[test]
800 fn test_validate_scale_range_valid() {
801 assert!(validate_scale_range(1, 10).is_ok());
802 assert!(validate_scale_range(1, 1).is_ok()); assert!(validate_scale_range(0, 5).is_ok());
804 assert!(validate_scale_range(5, 100).is_ok());
805 }
806
807 #[test]
808 fn test_validate_scale_range_min_greater_than_max() {
809 let result = validate_scale_range(10, 5);
810 assert!(result.is_err());
811 let err = result.unwrap_err();
812 assert!(matches!(
813 err.kind,
814 ValidationErrorKind::InvalidScaleRange { min: 10, max: 5 }
815 ));
816 }
817
818 #[test]
819 fn test_validate_scale_range_large_gap() {
820 assert!(validate_scale_range(1, 1000).is_ok());
822 }
823
824 #[test]
827 fn test_validate_schedule_wrapper_valid() {
828 assert!(validate_schedule_wrapper(&"0 0 0 * * * *".to_string()).is_ok()); assert!(validate_schedule_wrapper(&"0 */5 * * * * *".to_string()).is_ok()); assert!(validate_schedule_wrapper(&"0 0 12 * * MON-FRI *".to_string()).is_ok()); assert!(validate_schedule_wrapper(&"0 30 2 1 * * *".to_string()).is_ok()); assert!(validate_schedule_wrapper(&"*/10 * * * * * *".to_string()).is_ok());
834 }
836
837 #[test]
838 fn test_validate_schedule_wrapper_invalid() {
839 assert!(validate_schedule_wrapper(&"".to_string()).is_err()); assert!(validate_schedule_wrapper(&"not a cron".to_string()).is_err()); assert!(validate_schedule_wrapper(&"0 0 * * *".to_string()).is_err()); assert!(validate_schedule_wrapper(&"60 0 0 * * * *".to_string()).is_err());
844 }
846
847 #[test]
849 fn test_validate_secret_reference_plain_values() {
850 assert!(validate_secret_reference("my-value").is_ok());
852 assert!(validate_secret_reference("").is_ok());
853 assert!(validate_secret_reference("some string").is_ok());
854 assert!(validate_secret_reference("$E:MY_VAR").is_ok()); }
856
857 #[test]
858 fn test_validate_secret_reference_valid() {
859 assert!(validate_secret_reference("$S:my-secret").is_ok());
861 assert!(validate_secret_reference("$S:api_key").is_ok());
862 assert!(validate_secret_reference("$S:MySecret123").is_ok());
863 assert!(validate_secret_reference("$S:a").is_ok()); }
865
866 #[test]
867 fn test_validate_secret_reference_cross_service() {
868 assert!(validate_secret_reference("$S:@auth-service/jwt-secret").is_ok());
870 assert!(validate_secret_reference("$S:@my_service/api_key").is_ok());
871 assert!(validate_secret_reference("$S:@svc/secret").is_ok());
872 }
873
874 #[test]
875 fn test_validate_secret_reference_empty_after_prefix() {
876 assert!(validate_secret_reference("$S:").is_err());
878 }
879
880 #[test]
881 fn test_validate_secret_reference_must_start_with_letter() {
882 assert!(validate_secret_reference("$S:123-secret").is_err());
884 assert!(validate_secret_reference("$S:-my-secret").is_err());
885 assert!(validate_secret_reference("$S:_underscore").is_err());
886 }
887
888 #[test]
889 fn test_validate_secret_reference_invalid_chars() {
890 assert!(validate_secret_reference("$S:my.secret").is_err());
892 assert!(validate_secret_reference("$S:my secret").is_err());
893 assert!(validate_secret_reference("$S:my@secret").is_err());
894 }
895
896 #[test]
897 fn test_validate_secret_reference_cross_service_invalid() {
898 assert!(validate_secret_reference("$S:@service").is_err());
900 assert!(validate_secret_reference("$S:@/secret").is_err());
902 assert!(validate_secret_reference("$S:@service/").is_err());
904 assert!(validate_secret_reference("$S:@123-service/secret").is_err());
906 }
907}