Skip to content

Commit 79d7010

Browse files
committed
Support the explicit reconnect flag in shutdown API
1 parent cc08b45 commit 79d7010

4 files changed

Lines changed: 85 additions & 8 deletions

File tree

crates/hotfix-cli/src/lib.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{Context, Result};
22
use clap::{Parser, Subcommand};
33
use owo_colors::OwoColorize;
4-
use serde::Deserialize;
4+
use serde::{Deserialize, Serialize};
55

66
#[derive(Parser)]
77
#[command(name = "hotfix")]
@@ -29,7 +29,11 @@ pub enum Command {
2929
/// Request a reset on next logon
3030
Reset,
3131
/// Shutdown the session
32-
Shutdown,
32+
Shutdown {
33+
/// Whether to reconnect after shutdown
34+
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
35+
reconnect: bool,
36+
},
3337
}
3438

3539
#[derive(Debug, Deserialize)]
@@ -49,6 +53,11 @@ pub struct SessionInfo {
4953
pub status: String,
5054
}
5155

56+
#[derive(Debug, Serialize)]
57+
pub struct ShutdownRequest {
58+
pub reconnect: bool,
59+
}
60+
5261
pub async fn run(cli: Cli) -> Result<()> {
5362
let client = reqwest::Client::new();
5463
let base_url = cli.url.trim_end_matches('/');
@@ -125,10 +134,12 @@ pub async fn run(cli: Cli) -> Result<()> {
125134
);
126135
}
127136
}
128-
Command::Shutdown => {
137+
Command::Shutdown { reconnect } => {
129138
let url = format!("{}/api/shutdown", base_url);
139+
let body = ShutdownRequest { reconnect };
130140
let response = client
131141
.post(&url)
142+
.json(&body)
132143
.send()
133144
.await
134145
.context("Failed to send shutdown request")?;
@@ -235,7 +246,7 @@ mod tests {
235246

236247
let cli = Cli {
237248
url: mock_server.uri(),
238-
command: Command::Shutdown,
249+
command: Command::Shutdown { reconnect: true },
239250
};
240251

241252
let result = run(cli).await;
@@ -279,7 +290,7 @@ mod tests {
279290

280291
let cli = Cli {
281292
url: mock_server.uri(),
282-
command: Command::Shutdown,
293+
command: Command::Shutdown { reconnect: true },
283294
};
284295

285296
let result = run(cli).await;

crates/hotfix-cli/tests/integration.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use assert_cmd::assert::OutputAssertExt;
22
use assert_cmd::cargo_bin;
33
use predicates::prelude::*;
44
use std::process::Command;
5-
use wiremock::matchers::{method, path};
5+
use wiremock::matchers::{body_json, method, path};
66
use wiremock::{Mock, MockServer, ResponseTemplate};
77

88
#[tokio::test]
@@ -82,6 +82,7 @@ async fn test_cli_shutdown_command_integration() {
8282

8383
Mock::given(method("POST"))
8484
.and(path("/api/shutdown"))
85+
.and(body_json(serde_json::json!({"reconnect": true})))
8586
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
8687
.mount(&mock_server)
8788
.await;
@@ -95,6 +96,28 @@ async fn test_cli_shutdown_command_integration() {
9596
.stdout(predicate::str::contains("Shutdown requested successfully"));
9697
}
9798

99+
#[tokio::test]
100+
async fn test_cli_shutdown_command_with_reconnect_false() {
101+
let mock_server = MockServer::start().await;
102+
103+
Mock::given(method("POST"))
104+
.and(path("/api/shutdown"))
105+
.and(body_json(serde_json::json!({"reconnect": false})))
106+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
107+
.mount(&mock_server)
108+
.await;
109+
110+
let mut cmd = Command::new(cargo_bin!("hotfix"));
111+
cmd.arg("--url")
112+
.arg(mock_server.uri())
113+
.arg("shutdown")
114+
.arg("--reconnect")
115+
.arg("false")
116+
.assert()
117+
.success()
118+
.stdout(predicate::str::contains("Shutdown requested successfully"));
119+
}
120+
98121
#[tokio::test]
99122
async fn test_cli_help_command() {
100123
let mut cmd = Command::new(cargo_bin!("hotfix"));

crates/hotfix-web/src/endpoints/admin/shutdown.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ use crate::error::AppResult;
33
use crate::session_controller::SessionController;
44
use axum::Json;
55
use axum::extract::State;
6+
use serde::Deserialize;
7+
8+
#[derive(Deserialize)]
9+
pub(crate) struct ShutdownRequest {
10+
pub reconnect: bool,
11+
}
612

713
pub(crate) async fn shutdown<C: SessionController>(
814
State(state): State<AppState<C>>,
15+
Json(payload): Json<ShutdownRequest>,
916
) -> AppResult<Json<()>> {
10-
state.controller.shutdown(true).await?;
17+
state.controller.shutdown(payload.reconnect).await?;
1118

1219
Ok(Json(()))
1320
}

crates/hotfix-web/src/lib.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,19 @@ mod tests {
193193
self.request(Method::POST, path).await
194194
}
195195

196+
async fn post_json(&mut self, path: &str, json: Value) -> TestResponse {
197+
let body = serde_json::to_string(&json).unwrap();
198+
let request = Request::builder()
199+
.method(Method::POST)
200+
.uri(path)
201+
.header("Content-Type", "application/json")
202+
.body(Body::from(body))
203+
.unwrap();
204+
205+
let response = self.router.clone().oneshot(request).await.unwrap();
206+
TestResponse::new(response).await
207+
}
208+
196209
async fn request(&mut self, method: Method, path: &str) -> TestResponse {
197210
let request = Request::builder()
198211
.method(method)
@@ -309,7 +322,9 @@ mod tests {
309322
};
310323
let mut ctx = TestContext::with_config(config);
311324

312-
let response = ctx.post("/api/shutdown").await;
325+
let response = ctx
326+
.post_json("/api/shutdown", serde_json::json!({"reconnect": true}))
327+
.await;
313328

314329
response.assert_status(StatusCode::OK);
315330
let state = ctx.get_state();
@@ -321,6 +336,27 @@ mod tests {
321336
);
322337
}
323338

339+
#[tokio::test]
340+
async fn test_shutdown_endpoint_calls_shutdown_without_reconnect() {
341+
let config = RouterConfig {
342+
enable_admin_endpoints: true,
343+
};
344+
let mut ctx = TestContext::with_config(config);
345+
346+
let response = ctx
347+
.post_json("/api/shutdown", serde_json::json!({"reconnect": false}))
348+
.await;
349+
350+
response.assert_status(StatusCode::OK);
351+
let state = ctx.get_state();
352+
assert!(state.shutdown_called, "Shutdown should have been called");
353+
assert_eq!(
354+
state.shutdown_reconnect,
355+
Some(false),
356+
"Shutdown should be called with reconnect=false"
357+
);
358+
}
359+
324360
#[tokio::test]
325361
async fn test_admin_endpoints_disabled_by_default() {
326362
let mut ctx = TestContext::new(); // Default config has admin disabled

0 commit comments

Comments
 (0)