nixfleet_control_plane/server/routes/
fleet.rs1use std::sync::Arc;
12
13use axum::body::Bytes;
14use axum::extract::State;
15use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
16use axum::response::IntoResponse;
17
18use super::super::state::AppState;
19
20pub(in crate::server) async fn artifact(
23 State(state): State<Arc<AppState>>,
24) -> Result<impl IntoResponse, StatusCode> {
25 let snapshot_guard = state.verified_fleet.read().await;
26 let snapshot = snapshot_guard
27 .as_ref()
28 .ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
29 let bytes = Bytes::copy_from_slice(&snapshot.artifact_bytes);
30 let mut headers = HeaderMap::new();
31 headers.insert(
32 header::CONTENT_TYPE,
33 HeaderValue::from_static("application/json"),
34 );
35 Ok((StatusCode::OK, headers, bytes))
36}
37
38pub(in crate::server) async fn signature(
40 State(state): State<Arc<AppState>>,
41) -> Result<impl IntoResponse, StatusCode> {
42 let snapshot_guard = state.verified_fleet.read().await;
43 let snapshot = snapshot_guard
44 .as_ref()
45 .ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
46 let bytes = Bytes::copy_from_slice(&snapshot.signature_bytes);
47 let mut headers = HeaderMap::new();
48 headers.insert(
49 header::CONTENT_TYPE,
50 HeaderValue::from_static("application/octet-stream"),
51 );
52 Ok((StatusCode::OK, headers, bytes))
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::server::state::VerifiedFleetSnapshot;
59 use nixfleet_proto::FleetResolved;
60 use std::sync::Arc;
61
62 fn state_with_snapshot(snapshot: Option<VerifiedFleetSnapshot>) -> Arc<AppState> {
63 let state = AppState {
64 verified_fleet: Arc::new(tokio::sync::RwLock::new(snapshot)),
65 ..Default::default()
66 };
67 Arc::new(state)
68 }
69
70 fn sample_snapshot(artifact: &[u8], signature: &[u8]) -> VerifiedFleetSnapshot {
71 let fleet: FleetResolved = serde_json::from_str(
74 r#"{"schemaVersion":1,"hosts":{},"channels":{},"waves":{},"meta":{"schemaVersion":1}}"#,
75 )
76 .expect("minimal FleetResolved JSON valid");
77 VerifiedFleetSnapshot {
78 fleet: Arc::new(fleet),
79 fleet_resolved_hash: "0".repeat(64),
80 artifact_bytes: artifact.to_vec(),
81 signature_bytes: signature.to_vec(),
82 }
83 }
84
85 #[tokio::test]
86 async fn artifact_returns_503_when_verified_fleet_unset() {
87 let state = state_with_snapshot(None);
88 match artifact(State(state)).await {
89 Err(status) => assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE),
90 Ok(_) => panic!("expected SERVICE_UNAVAILABLE when verified_fleet is None"),
91 }
92 }
93
94 #[tokio::test]
95 async fn signature_returns_503_when_verified_fleet_unset() {
96 let state = state_with_snapshot(None);
97 match signature(State(state)).await {
98 Err(status) => assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE),
99 Ok(_) => panic!("expected SERVICE_UNAVAILABLE when verified_fleet is None"),
100 }
101 }
102
103 #[tokio::test]
104 async fn artifact_returns_snapshot_bytes_on_200() {
105 let snap = sample_snapshot(br#"{"hello":"fleet"}"#, &[0xDE, 0xAD]);
106 let state = state_with_snapshot(Some(snap));
107 let resp = artifact(State(state))
108 .await
109 .expect("artifact returns Ok")
110 .into_response();
111 assert_eq!(resp.status(), StatusCode::OK);
112 let body = axum::body::to_bytes(resp.into_body(), 4096).await.unwrap();
113 assert_eq!(&body[..], br#"{"hello":"fleet"}"#);
114 }
115
116 #[tokio::test]
117 async fn signature_returns_snapshot_bytes_on_200() {
118 let snap = sample_snapshot(b"{}", &[0xDE, 0xAD, 0xBE, 0xEF]);
119 let state = state_with_snapshot(Some(snap));
120 let resp = signature(State(state))
121 .await
122 .expect("signature returns Ok")
123 .into_response();
124 assert_eq!(resp.status(), StatusCode::OK);
125 let body = axum::body::to_bytes(resp.into_body(), 4096).await.unwrap();
126 assert_eq!(&body[..], &[0xDE, 0xAD, 0xBE, 0xEF]);
127 }
128}