torrust_tracker_deployer_lib/domain/environment/state/
destroy_failed.rs1use serde::{Deserialize, Serialize};
15
16use crate::domain::environment::state::{AnyEnvironmentState, BaseFailureContext, StateTypeError};
17use crate::domain::environment::Environment;
18use crate::shared::ErrorKind;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct DestroyFailureContext {
30 pub failed_step: DestroyStep,
32
33 pub error_kind: ErrorKind,
35
36 #[serde(flatten)]
38 pub base: BaseFailureContext,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum DestroyStep {
44 LoadEnvironment,
46 DestroyInfrastructure,
48 CleanupStateFiles,
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63pub struct DestroyFailed {
64 pub context: DestroyFailureContext,
66}
67
68impl Environment<DestroyFailed> {
70 #[must_use]
72 pub fn into_any(self) -> AnyEnvironmentState {
73 AnyEnvironmentState::DestroyFailed(self)
74 }
75}
76
77impl AnyEnvironmentState {
79 pub fn try_into_destroy_failed(self) -> Result<Environment<DestroyFailed>, StateTypeError> {
85 match self {
86 Self::DestroyFailed(env) => Ok(env),
87 other => Err(StateTypeError::UnexpectedState {
88 expected: "destroy_failed",
89 actual: other.state_name().to_string(),
90 }),
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::domain::environment::TraceId;
99 use chrono::Utc;
100 use std::path::PathBuf;
101 use std::time::Duration;
102
103 fn create_test_context() -> DestroyFailureContext {
104 DestroyFailureContext {
105 failed_step: DestroyStep::DestroyInfrastructure,
106 error_kind: ErrorKind::InfrastructureOperation,
107 base: BaseFailureContext {
108 error_summary: "infrastructure_destroy_failed".to_string(),
109 failed_at: Utc::now(),
110 execution_started_at: Utc::now(),
111 execution_duration: Duration::from_secs(30),
112 trace_id: TraceId::new(),
113 trace_file_path: None,
114 },
115 }
116 }
117
118 #[test]
119 fn it_should_create_destroy_failed_state_with_context() {
120 let context = create_test_context();
121 let state = DestroyFailed {
122 context: context.clone(),
123 };
124 assert_eq!(
125 state.context.failed_step,
126 DestroyStep::DestroyInfrastructure
127 );
128 assert_eq!(state.context.error_kind, ErrorKind::InfrastructureOperation);
129 }
130
131 #[test]
132 fn it_should_clone_destroy_failed_state() {
133 let state = DestroyFailed {
134 context: create_test_context(),
135 };
136 let cloned = state.clone();
137 assert_eq!(state.context.failed_step, cloned.context.failed_step);
138 }
139
140 #[test]
141 fn it_should_serialize_destroy_failed_state_to_json() {
142 let state = DestroyFailed {
143 context: create_test_context(),
144 };
145 let json = serde_json::to_string(&state).unwrap();
146 assert!(json.contains("DestroyInfrastructure"));
147 assert!(json.contains("InfrastructureOperation"));
148 }
149
150 #[test]
151 fn it_should_deserialize_destroy_failed_state_from_json() {
152 let state = DestroyFailed {
153 context: create_test_context(),
154 };
155 let json = serde_json::to_string(&state).unwrap();
156 let deserialized: DestroyFailed = serde_json::from_str(&json).unwrap();
157 assert_eq!(
158 deserialized.context.failed_step,
159 DestroyStep::DestroyInfrastructure
160 );
161 }
162
163 mod conversion_tests {
164 use super::*;
165 use crate::adapters::ssh::SshCredentials;
166 use crate::domain::environment::name::EnvironmentName;
167 use crate::domain::environment::runtime_outputs::ProvisionMethod;
168 use crate::domain::provider::{LxdConfig, ProviderConfig};
169 use crate::domain::ProfileName;
170 use crate::shared::Username;
171 use std::net::{IpAddr, Ipv4Addr};
172 use std::path::PathBuf;
173
174 fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
175 ProviderConfig::Lxd(LxdConfig {
176 profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
177 })
178 }
179
180 fn create_test_ssh_credentials() -> SshCredentials {
181 let username = Username::new("test-user".to_string()).unwrap();
182 SshCredentials::new(
183 PathBuf::from("/tmp/test_key"),
184 PathBuf::from("/tmp/test_key.pub"),
185 username,
186 )
187 }
188
189 fn create_test_environment_destroy_failed() -> Environment<DestroyFailed> {
190 let name = EnvironmentName::new("test-env".to_string()).unwrap();
191 let ssh_creds = create_test_ssh_credentials();
192 Environment::new(
193 name.clone(),
194 default_lxd_provider_config(&name),
195 ssh_creds,
196 22,
197 chrono::Utc::now(),
198 )
199 .start_provisioning()
200 .provisioned(
201 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
202 ProvisionMethod::Provisioned,
203 )
204 .start_destroying()
205 .destroy_failed(super::create_test_context())
206 }
207
208 #[test]
209 fn it_should_convert_destroy_failed_environment_into_any() {
210 let env = create_test_environment_destroy_failed();
211 let any_env = env.into_any();
212 assert!(matches!(any_env, AnyEnvironmentState::DestroyFailed(_)));
213 }
214
215 #[test]
216 fn it_should_convert_any_to_destroy_failed_successfully() {
217 let env = create_test_environment_destroy_failed();
218 let any_env = env.into_any();
219 let result = any_env.try_into_destroy_failed();
220 assert!(result.is_ok());
221 }
222
223 #[test]
224 fn it_should_preserve_error_details_in_failed_states() {
225 let name = EnvironmentName::new("test-env".to_string()).unwrap();
226 let ssh_creds = create_test_ssh_credentials();
227 let context = super::create_test_context();
228 let env = Environment::new(
229 name.clone(),
230 default_lxd_provider_config(&name),
231 ssh_creds,
232 22,
233 chrono::Utc::now(),
234 )
235 .start_provisioning()
236 .provisioned(
237 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
238 ProvisionMethod::Provisioned,
239 )
240 .start_destroying()
241 .destroy_failed(context.clone());
242
243 let any_env = env.into_any();
245 let env_restored = any_env.try_into_destroy_failed().unwrap();
246
247 assert_eq!(
248 env_restored.state().context.failed_step,
249 context.failed_step
250 );
251 assert_eq!(
252 env_restored.state().context.base.error_summary,
253 context.base.error_summary
254 );
255 }
256 }
257
258 mod context_tests {
259 use super::*;
260
261 #[test]
262 fn it_should_serialize_destroy_failure_context() {
263 let context = DestroyFailureContext {
264 failed_step: DestroyStep::CleanupStateFiles,
265 error_kind: ErrorKind::StatePersistence,
266 base: BaseFailureContext {
267 error_summary: "Failed to clean up state files".to_string(),
268 failed_at: Utc::now(),
269 execution_started_at: Utc::now(),
270 execution_duration: Duration::from_secs(5),
271 trace_id: TraceId::new(),
272 trace_file_path: Some(PathBuf::from("/data/env/traces/trace.log")),
273 },
274 };
275
276 let json = serde_json::to_string(&context).unwrap();
277 assert!(json.contains("CleanupStateFiles"));
278 assert!(json.contains("StatePersistence"));
279 }
280
281 #[test]
282 fn it_should_deserialize_destroy_failure_context() {
283 let trace_id = TraceId::new();
284 let json = format!(
285 r#"{{
286 "failed_step": "LoadEnvironment",
287 "error_kind": "StatePersistence",
288 "error_summary": "Failed to load environment",
289 "failed_at": "2025-10-24T10:00:00Z",
290 "execution_started_at": "2025-10-24T09:59:00Z",
291 "execution_duration": {{"secs": 60, "nanos": 0}},
292 "trace_id": "{trace_id}",
293 "trace_file_path": null
294 }}"#
295 );
296
297 let context: DestroyFailureContext = serde_json::from_str(&json).unwrap();
298 assert_eq!(context.failed_step, DestroyStep::LoadEnvironment);
299 assert_eq!(context.error_kind, ErrorKind::StatePersistence);
300 }
301 }
302}