headless_lms_server/controllers/mock_suotar/
control.rs1use crate::domain::credit_registration_phases::{
11 CreditRegistrationPhase, PhaseTick, run_phase_once,
12};
13use crate::prelude::*;
14use sqlx::PgPool;
15
16#[derive(Debug, Deserialize)]
17pub struct RunTickQuery {
18 pub phase: Option<String>,
20}
21
22#[derive(Debug, Serialize, Deserialize, PartialEq)]
24#[serde(rename_all = "camelCase", tag = "status")]
25pub enum PhaseTickResult {
26 Ran {
27 phase: String,
28 items_processed: i32,
29 items_failed: i32,
30 error: Option<String>,
32 },
33 PhaseNotImplemented { phase: String },
35 UnknownPhase {
37 phase: Option<String>,
38 known_phases: Vec<String>,
39 },
40}
41
42impl PhaseTickResult {
43 fn of(phase: CreditRegistrationPhase, tick: PhaseTick) -> Self {
44 match tick {
45 PhaseTick::Ran(outcome) => Self::Ran {
46 phase: phase.as_str().to_string(),
47 items_processed: outcome.items_processed,
48 items_failed: outcome.items_failed,
49 error: outcome.error,
50 },
51 PhaseTick::NotImplemented => Self::PhaseNotImplemented {
52 phase: phase.as_str().to_string(),
53 },
54 }
55 }
56}
57
58#[derive(Debug, Serialize, Deserialize, PartialEq)]
60#[serde(rename_all = "camelCase")]
61pub struct RegistrarTickResult {
62 pub phases: Vec<PhaseTickResult>,
63}
64
65async fn run_tick(
72 app_conf: web::Data<ApplicationConfiguration>,
73 pool: web::Data<PgPool>,
74 query: web::Query<RunTickQuery>,
75) -> ControllerResult<HttpResponse> {
76 super::assert_enabled(&app_conf);
77 let token = skip_authorize();
78
79 let Some(phase) = query
80 .phase
81 .as_deref()
82 .and_then(CreditRegistrationPhase::from_phase_name)
83 else {
84 return token.authorized_ok(HttpResponse::BadRequest().json(
85 PhaseTickResult::UnknownPhase {
86 phase: query.phase.clone(),
87 known_phases: known_phase_names(),
88 },
89 ));
90 };
91
92 let result = PhaseTickResult::of(phase, run_phase_once(&pool, phase).await?);
93 token.authorized_ok(match &result {
94 PhaseTickResult::Ran { .. } => HttpResponse::Ok().json(&result),
95 _ => HttpResponse::NotImplemented().json(&result),
96 })
97}
98
99async fn run_registrar_tick(
107 app_conf: web::Data<ApplicationConfiguration>,
108 pool: web::Data<PgPool>,
109) -> ControllerResult<HttpResponse> {
110 super::assert_enabled(&app_conf);
111 let token = skip_authorize();
112
113 let mut phases = Vec::new();
114 for phase in CreditRegistrationPhase::REGISTRAR_TICK_SEQUENCE {
115 phases.push(PhaseTickResult::of(
116 phase,
117 run_phase_once(&pool, phase).await?,
118 ));
119 }
120 token.authorized_ok(HttpResponse::Ok().json(RegistrarTickResult { phases }))
121}
122
123fn known_phase_names() -> Vec<String> {
124 CreditRegistrationPhase::ALL
125 .iter()
126 .map(|phase| phase.as_str().to_string())
127 .collect()
128}
129
130pub fn _add_routes(cfg: &mut ServiceConfig) {
131 cfg.route("/run-tick", web::post().to(run_tick))
132 .route("/run-registrar-tick", web::post().to(run_registrar_tick));
133}
134
135#[cfg(test)]
136mod tests {
137 use actix_web::{App, http::StatusCode, test, web::Data};
138 use headless_lms_base::config::{OAuthServerConfiguration, SuotarConfiguration};
139 use secrecy::{SecretBox, SecretString};
140 use std::sync::Arc;
141
142 use super::*;
143 use crate::controllers::configure_controllers;
144
145 fn app_conf(test_suotar: bool) -> ApplicationConfiguration {
147 ApplicationConfiguration {
148 base_url: "http://project-331.local".to_string(),
149 test_mode: true,
150 test_chatbot: false,
151 test_sisu: false,
152 test_suotar,
153 development_uuid_login: false,
154 enable_admin_email_verification: false,
155 enable_email_ownership_verification: false,
156 azure_configuration: None,
157 suotar_configuration: SuotarConfiguration::mock_conf("http://project-331.local")
158 .expect("the mock configuration is built from a constant base url"),
159 tmc_account_creation_origin: None,
160 tmc_admin_access_token: SecretString::new("mock-access-token".to_string().into()),
161 oauth_server_configuration: OAuthServerConfiguration {
162 rsa_public_key: "test".into(),
163 rsa_private_key: SecretString::new("test".into()),
164 oauth_token_hmac_key: SecretString::new("test".into()),
165 dpop_nonce_key: Arc::new(SecretBox::new(Box::new("test".into()))),
166 },
167 }
168 }
169
170 async fn call_run_tick(
173 test_suotar: bool,
174 query: &str,
175 ) -> actix_web::dev::ServiceResponse<actix_web::body::BoxBody> {
176 let app_conf = Data::new(app_conf(test_suotar));
177 let pool = Data::new(
178 PgPool::connect_lazy("postgres://headless-lms@localhost:54328/headless_lms_dev")
179 .expect("a lazy pool only parses the url"),
180 );
181 let service = test::init_service(
182 App::new()
183 .app_data(pool)
184 .app_data(app_conf.clone())
185 .service(
186 web::scope("/api/v0")
187 .configure(|cfg| configure_controllers(cfg, app_conf.clone())),
188 ),
189 )
190 .await;
191 let req = test::TestRequest::post()
192 .uri(&format!("/api/v0/mock-suotar/control/run-tick{query}"))
193 .to_request();
194 test::call_service(&service, req).await
195 }
196
197 #[actix_web::test]
198 async fn run_tick_reports_phase_not_implemented_when_the_mock_is_enabled() {
199 let res = call_run_tick(true, "?phase=verify").await;
200 assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED);
201 let body: PhaseTickResult = test::read_body_json(res).await;
202 assert_eq!(
203 body,
204 PhaseTickResult::PhaseNotImplemented {
205 phase: "verify".to_string()
206 }
207 );
208 }
209
210 #[actix_web::test]
212 async fn run_tick_is_absent_when_the_mock_is_disabled() {
213 let res = call_run_tick(false, "?phase=verify").await;
214 assert_eq!(res.status(), StatusCode::NOT_FOUND);
215 }
216
217 #[actix_web::test]
218 async fn every_canonical_phase_name_dispatches() {
219 for phase in CreditRegistrationPhase::ALL {
220 let res = call_run_tick(true, &format!("?phase={}", phase.as_str())).await;
221 assert_eq!(
222 res.status(),
223 StatusCode::NOT_IMPLEMENTED,
224 "phase {} did not dispatch",
225 phase.as_str()
226 );
227 }
228 }
229
230 #[actix_web::test]
231 async fn an_invented_phase_name_is_rejected() {
232 let res = call_run_tick(true, "?phase=materialise").await;
233 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
234 let body: PhaseTickResult = test::read_body_json(res).await;
235 assert_eq!(
236 body,
237 PhaseTickResult::UnknownPhase {
238 phase: Some("materialise".to_string()),
239 known_phases: known_phase_names(),
240 }
241 );
242 }
243}