torrust_tracker_deployer_lib/domain/environment/state/
run_failed.rs1use std::fmt;
14
15use serde::{Deserialize, Serialize};
16
17use crate::domain::environment::state::{AnyEnvironmentState, BaseFailureContext, StateTypeError};
18use crate::domain::environment::Environment;
19use crate::shared::error::ErrorKind;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum RunStep {
33 StartServices,
35}
36
37impl fmt::Display for RunStep {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 let name = match self {
40 Self::StartServices => "Start Services",
41 };
42 write!(f, "{name}")
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub struct RunFailureContext {
59 pub failed_step: RunStep,
61
62 pub error_kind: ErrorKind,
64
65 pub base: BaseFailureContext,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct RunFailed {
79 pub context: RunFailureContext,
81}
82
83impl Environment<RunFailed> {
85 #[must_use]
87 pub fn into_any(self) -> AnyEnvironmentState {
88 AnyEnvironmentState::RunFailed(self)
89 }
90}
91
92impl AnyEnvironmentState {
94 pub fn try_into_run_failed(self) -> Result<Environment<RunFailed>, StateTypeError> {
100 match self {
101 Self::RunFailed(env) => Ok(env),
102 other => Err(StateTypeError::UnexpectedState {
103 expected: "run_failed",
104 actual: other.state_name().to_string(),
105 }),
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use std::time::Duration;
113
114 use chrono::Utc;
115
116 use super::*;
117 use crate::domain::environment::TraceId;
118
119 fn create_test_failure_context() -> RunFailureContext {
120 let now = Utc::now();
121 RunFailureContext {
122 failed_step: RunStep::StartServices,
123 error_kind: ErrorKind::InfrastructureOperation,
124 base: BaseFailureContext {
125 error_summary: "Test error".to_string(),
126 failed_at: now,
127 execution_started_at: now,
128 execution_duration: Duration::from_secs(10),
129 trace_id: TraceId::new(),
130 trace_file_path: None,
131 },
132 }
133 }
134
135 #[test]
136 fn it_should_create_run_failed_state_with_context() {
137 let context = create_test_failure_context();
138 let state = RunFailed {
139 context: context.clone(),
140 };
141 assert_eq!(state.context.failed_step, RunStep::StartServices);
142 assert_eq!(state.context.error_kind, ErrorKind::InfrastructureOperation);
143 }
144
145 #[test]
146 fn it_should_display_run_step() {
147 assert_eq!(format!("{}", RunStep::StartServices), "Start Services");
148 }
149
150 #[test]
151 fn it_should_serialize_run_step_to_snake_case() {
152 let step = RunStep::StartServices;
153 let json = serde_json::to_string(&step).unwrap();
154 assert_eq!(json, r#""start_services""#);
155 }
156
157 #[test]
158 fn it_should_deserialize_run_step_from_snake_case() {
159 let step: RunStep = serde_json::from_str(r#""start_services""#).unwrap();
160 assert_eq!(step, RunStep::StartServices);
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_run_failed() -> Environment<RunFailed> {
190 let name = EnvironmentName::new("test-env".to_string()).unwrap();
191 let ssh_creds = create_test_ssh_credentials();
192 let now = Utc::now();
193 let context = RunFailureContext {
194 failed_step: RunStep::StartServices,
195 error_kind: ErrorKind::InfrastructureOperation,
196 base: BaseFailureContext {
197 error_summary: "Docker compose failed".to_string(),
198 failed_at: now,
199 execution_started_at: now,
200 execution_duration: Duration::from_secs(5),
201 trace_id: TraceId::new(),
202 trace_file_path: None,
203 },
204 };
205 Environment::new(
206 name.clone(),
207 default_lxd_provider_config(&name),
208 ssh_creds,
209 22,
210 chrono::Utc::now(),
211 )
212 .start_provisioning()
213 .provisioned(
214 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
215 ProvisionMethod::Provisioned,
216 )
217 .start_configuring()
218 .configured()
219 .start_releasing()
220 .released()
221 .start_running()
222 .run_failed(context)
223 }
224
225 #[test]
226 fn it_should_convert_run_failed_environment_into_any() {
227 let env = create_test_environment_run_failed();
228 let any_env = env.into_any();
229 assert!(matches!(any_env, AnyEnvironmentState::RunFailed(_)));
230 }
231
232 #[test]
233 fn it_should_convert_any_to_run_failed_successfully() {
234 let env = create_test_environment_run_failed();
235 let any_env = env.into_any();
236 let result = any_env.try_into_run_failed();
237 assert!(result.is_ok());
238 }
239 }
240}