switchgear_service/discovery/
service.rs1use crate::api::discovery::DiscoveryBackendStore;
2use crate::api::service::StatusCode;
3use crate::axum::auth::BearerTokenAuthLayer;
4use crate::discovery::auth::DiscoveryBearerTokenValidator;
5use crate::discovery::handler::DiscoveryHandlers;
6use crate::discovery::state::DiscoveryState;
7use axum::routing::{delete, get, patch, post, put};
8use axum::Router;
9
10#[derive(Debug)]
11pub struct DiscoveryService;
12
13impl DiscoveryService {
14 pub fn router<S>(state: DiscoveryState<S>) -> Router
15 where
16 S: DiscoveryBackendStore + Clone + Send + Sync + 'static,
17 {
18 Router::new()
19 .route(
20 "/discovery/{addr_variant}/{addr_value}",
21 get(DiscoveryHandlers::get_backend),
22 )
23 .route(
24 "/discovery/{addr_variant}/{addr_value}",
25 put(DiscoveryHandlers::put_backend),
26 )
27 .route(
28 "/discovery/{addr_variant}/{addr_value}",
29 patch(DiscoveryHandlers::patch_backend),
30 )
31 .route(
32 "/discovery/{addr_variant}/{addr_value}",
33 delete(DiscoveryHandlers::delete_backend),
34 )
35 .route("/discovery", get(DiscoveryHandlers::get_backends))
36 .route("/discovery", post(DiscoveryHandlers::post_backend))
37 .layer(BearerTokenAuthLayer::new(
38 DiscoveryBearerTokenValidator::new(state.auth_authority().clone()),
39 "discovery",
40 ))
41 .route("/health", get(Self::health_check_handler))
42 .with_state(state)
43 }
44
45 async fn health_check_handler() -> StatusCode {
46 StatusCode::OK
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use crate::api::discovery::{
53 DiscoveryBackend, DiscoveryBackendAddress, DiscoveryBackendImplementation,
54 DiscoveryBackendPatchSparse, DiscoveryBackendSparse,
55 };
56 use crate::components::discovery::memory::MemoryDiscoveryBackendStore;
57 use crate::discovery::auth::{DiscoveryAudience, DiscoveryClaims};
58 use crate::discovery::service::DiscoveryService;
59 use crate::discovery::state::DiscoveryState;
60 use axum::http::StatusCode;
61 use axum_test::TestServer;
62 use jsonwebtoken::{encode, Algorithm, DecodingKey, EncodingKey, Header};
63 use p256::ecdsa::SigningKey;
64 use p256::pkcs8::EncodePrivateKey;
65 use p256::pkcs8::EncodePublicKey;
66 use rand::thread_rng;
67 use std::time::{SystemTime, UNIX_EPOCH};
68
69 fn create_test_backend(partition: &str, address: &str) -> DiscoveryBackend {
70 DiscoveryBackend {
71 address: DiscoveryBackendAddress::Url(format!("https://{address}").parse().unwrap()),
72 backend: DiscoveryBackendSparse {
73 name: None,
74 partitions: [partition.to_string()].into(),
75 weight: 100,
76 enabled: true,
77 implementation: DiscoveryBackendImplementation::RemoteHttp,
78 },
79 }
80 }
81
82 struct TestServerWithAuthorization {
83 server: TestServer,
84 authorization: String,
85 }
86
87 async fn setup_test_server() -> TestServerWithAuthorization {
88 let mut rng = thread_rng();
89 let private_key = SigningKey::random(&mut rng);
90 let public_key = *private_key.verifying_key();
91
92 let private_key = private_key
93 .to_pkcs8_pem(p256::pkcs8::LineEnding::default())
94 .unwrap();
95 let encoding_key = EncodingKey::from_ec_pem(private_key.as_bytes()).unwrap();
96
97 let public_key = public_key
98 .to_public_key_pem(p256::pkcs8::LineEnding::default())
99 .unwrap();
100 let decoding_key = DecodingKey::from_ec_pem(public_key.as_bytes()).unwrap();
101
102 let store = MemoryDiscoveryBackendStore::new();
103 let state = DiscoveryState::new(store, decoding_key);
104
105 let header = Header::new(Algorithm::ES256);
106 let claims = DiscoveryClaims {
107 aud: DiscoveryAudience::Discovery,
108 exp: (SystemTime::now()
109 .duration_since(UNIX_EPOCH)
110 .unwrap()
111 .as_secs()
112 + 3600) as usize,
113 };
114 let authorization = encode(&header, &claims, &encoding_key).unwrap();
115
116 let app = DiscoveryService::router(state);
117 TestServerWithAuthorization {
118 server: TestServer::new(app).unwrap(),
119 authorization,
120 }
121 }
122
123 #[tokio::test]
124 async fn health_check_when_called_then_returns_ok() {
125 let server = setup_test_server().await;
126
127 let response = server.server.get("/health").await;
128
129 assert_eq!(response.status_code(), StatusCode::OK);
130 assert_eq!(response.text(), "");
132 }
133
134 #[tokio::test]
135 async fn get_backends_when_empty_then_returns_empty_list() {
136 let server = setup_test_server().await;
137
138 let response = server
139 .server
140 .get("/discovery")
141 .authorization_bearer(server.authorization.clone())
142 .await;
143
144 assert_eq!(response.status_code(), StatusCode::OK);
145 let backends: Vec<DiscoveryBackend> = response.json();
146 assert!(backends.is_empty());
147
148 assert_eq!(
150 response.header("cache-control"),
151 "no-store, no-cache, must-revalidate"
152 );
153 assert_eq!(response.header("expires"), "Thu, 01 Jan 1970 00:00:00 GMT");
154 assert_eq!(response.header("pragma"), "no-cache");
155 }
156
157 #[tokio::test]
158 async fn post_backend_when_new_then_creates_and_returns_location() {
159 let server = setup_test_server().await;
160 let backend = create_test_backend("default", "192.168.1.1:8080");
161
162 let response = server
163 .server
164 .post("/discovery")
165 .authorization_bearer(server.authorization.clone())
166 .json(&backend)
167 .await;
168
169 assert_eq!(response.status_code(), StatusCode::CREATED);
170 let location = response.header("location");
171 assert!(location.to_str().unwrap().contains("url/"));
172 }
173
174 #[tokio::test]
175 async fn post_backend_when_duplicate_then_returns_conflict() {
176 let server = setup_test_server().await;
177 let backend = create_test_backend("default", "192.168.1.1:8080");
178
179 let response1 = server
181 .server
182 .post("/discovery")
183 .authorization_bearer(server.authorization.clone())
184 .json(&backend)
185 .await;
186 assert_eq!(response1.status_code(), StatusCode::CREATED);
187
188 let response2 = server
190 .server
191 .post("/discovery")
192 .authorization_bearer(server.authorization.clone())
193 .json(&backend)
194 .await;
195 assert_eq!(response2.status_code(), StatusCode::CONFLICT);
196 }
197
198 #[tokio::test]
199 async fn get_backend_when_exists_then_returns_backend() {
200 let server = setup_test_server().await;
201 let backend = create_test_backend("default", "192.168.1.1:8080");
202
203 let response = server
205 .server
206 .post("/discovery")
207 .authorization_bearer(server.authorization.clone())
208 .json(&backend)
209 .await;
210
211 let location = response.header("location");
212 let location = location.to_str().unwrap();
213
214 let response = server
216 .server
217 .get(format!("/discovery/{location}").as_str())
218 .authorization_bearer(server.authorization.clone())
219 .await;
220
221 assert_eq!(response.status_code(), StatusCode::OK);
222 let retrieved: DiscoveryBackend = response.json();
223 assert_eq!(
224 retrieved.backend.implementation,
225 DiscoveryBackendImplementation::RemoteHttp
226 );
227 assert_eq!(retrieved.address, backend.address);
228 }
229
230 #[tokio::test]
231 async fn get_backend_when_not_exists_then_returns_not_found() {
232 let server = setup_test_server().await;
233
234 let response = server
235 .server
236 .get("/discovery/default/inet/MTkyLjE2OC4xLjE6ODA4MA")
237 .authorization_bearer(server.authorization.clone())
238 .await;
239
240 assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
241 }
242
243 #[tokio::test]
244 async fn put_backend_when_new_then_created() {
245 let server = setup_test_server().await;
246 let backend = create_test_backend("default", "192.168.1.1:8080");
247
248 let response = server
249 .server
250 .put("/discovery/url/aHR0cHM6Ly8xOTIuMTY4LjEuMTo4MDgwLw")
251 .authorization_bearer(server.authorization.clone())
252 .json(&backend.backend)
253 .await;
254
255 assert_eq!(response.status_code(), StatusCode::CREATED);
256 }
257
258 #[tokio::test]
259 async fn put_backend_when_exists_then_updates_no_content() {
260 let server = setup_test_server().await;
261 let mut backend = create_test_backend("default", "192.168.1.1:8080");
262
263 let response = server
265 .server
266 .post("/discovery")
267 .authorization_bearer(server.authorization.clone())
268 .json(&backend)
269 .await;
270
271 let location = response.header("location");
272 let location = location.to_str().unwrap();
273
274 backend.backend.weight = 200;
276 let response = server
277 .server
278 .put(&format!("/discovery/{location}"))
279 .authorization_bearer(server.authorization.clone())
280 .json(&backend.backend)
281 .await;
282
283 assert_eq!(response.status_code(), StatusCode::NO_CONTENT);
284
285 let get_response = server
287 .server
288 .get(&format!("/discovery/{location}"))
289 .authorization_bearer(server.authorization.clone())
290 .await;
291 let updated: DiscoveryBackend = get_response.json();
292 assert_eq!(updated.backend.weight, 200);
293 }
294
295 #[tokio::test]
296 async fn patch_backend_then_no_content() {
297 let server = setup_test_server().await;
298 let mut backend = create_test_backend("default", "192.168.1.1:8080");
299
300 let response = server
302 .server
303 .post("/discovery")
304 .authorization_bearer(server.authorization.clone())
305 .json(&backend)
306 .await;
307
308 let location = response.header("location");
309 let location = location.to_str().unwrap();
310
311 let patch = DiscoveryBackendPatchSparse {
312 name: None,
313 partitions: None,
314 weight: Some(200),
315 enabled: None,
316 };
317 backend.backend.weight = 200;
319 let response = server
320 .server
321 .patch(&format!("/discovery/{location}"))
322 .authorization_bearer(server.authorization.clone())
323 .json(&patch)
324 .await;
325
326 assert_eq!(response.status_code(), StatusCode::NO_CONTENT);
327
328 let get_response = server
330 .server
331 .get(&format!("/discovery/{location}"))
332 .authorization_bearer(server.authorization.clone())
333 .await;
334 let updated: DiscoveryBackend = get_response.json();
335 assert_eq!(updated.backend.weight, 200);
336 }
337
338 #[tokio::test]
339 async fn patch_missing_backend_then_not_found() {
340 let server = setup_test_server().await;
341 let mut backend = create_test_backend("default", "192.168.1.1:8080");
342
343 let location = backend.address.encoded();
344
345 let patch = DiscoveryBackendPatchSparse {
346 name: None,
347 partitions: None,
348 weight: Some(200),
349 enabled: None,
350 };
351 backend.backend.weight = 200;
353 let response = server
354 .server
355 .patch(&format!("/discovery/{location}"))
356 .authorization_bearer(server.authorization.clone())
357 .json(&patch)
358 .await;
359
360 assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
361 }
362
363 #[tokio::test]
364 async fn delete_backend_when_exists_then_removes_and_returns_backend() {
365 let server = setup_test_server().await;
366 let backend = create_test_backend("default", "192.168.1.1:8080");
367
368 let response = server
370 .server
371 .post("/discovery")
372 .authorization_bearer(server.authorization.clone())
373 .json(&backend)
374 .await;
375 let location = response.header("location");
376 let location = location.to_str().unwrap();
377
378 let response = server
380 .server
381 .delete(&format!("/discovery/{location}"))
382 .authorization_bearer(server.authorization.clone())
383 .await;
384 eprintln!("location: {location}");
385
386 assert_eq!(response.status_code(), StatusCode::NO_CONTENT);
387 let get_response = server
391 .server
392 .get(&format!("/discovery/{location}"))
393 .authorization_bearer(server.authorization.clone())
394 .await;
395 assert_eq!(get_response.status_code(), StatusCode::NOT_FOUND);
396 }
397
398 #[tokio::test]
399 async fn delete_backend_when_not_exists_then_returns_not_found() {
400 let server = setup_test_server().await;
401
402 let response = server
403 .server
404 .delete("/discovery/default/inet/MTkyLjE2OC4xLjE6ODA4MA")
405 .authorization_bearer(server.authorization.clone())
406 .await;
407
408 assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
409 }
410
411 #[tokio::test]
412 async fn get_backends_when_multiple_exist_then_returns_all() {
413 let server = setup_test_server().await;
414 let backend1 = create_test_backend("default", "192.168.1.1:8080");
415 let backend2 = create_test_backend("default", "192.168.1.2:8080");
416
417 server
419 .server
420 .post("/discovery")
421 .authorization_bearer(server.authorization.clone())
422 .json(&backend1)
423 .await;
424 server
425 .server
426 .post("/discovery")
427 .authorization_bearer(server.authorization.clone())
428 .json(&backend2)
429 .await;
430
431 let response = server
433 .server
434 .get("/discovery")
435 .authorization_bearer(server.authorization.clone())
436 .await;
437
438 assert_eq!(response.status_code(), StatusCode::OK);
439 let backends: Vec<DiscoveryBackend> = response.json();
440 assert_eq!(backends.len(), 2);
441 }
442
443 #[tokio::test]
444 async fn api_when_invalid_json_then_returns_bad_request() {
445 let server = setup_test_server().await;
446
447 let response = server
448 .server
449 .post("/discovery")
450 .authorization_bearer(server.authorization.clone())
451 .text("invalid json")
452 .await;
453
454 assert_eq!(response.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
455 }
456
457 #[tokio::test]
458 async fn api_when_invalid_address_encoding_then_returns_bad_request() {
459 let server = setup_test_server().await;
460
461 let response = server
462 .server
463 .get("/discovery/default/inet/invalid_base64")
464 .authorization_bearer(server.authorization.clone())
465 .await;
466
467 assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
468 }
469
470 #[tokio::test]
471 async fn api_when_unsupported_variant_then_returns_bad_request() {
472 let server = setup_test_server().await;
473
474 let response = server
475 .server
476 .get("/discovery/default/unsupported/dGVzdA")
477 .authorization_bearer(server.authorization.clone())
478 .await;
479
480 assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
481 }
482
483 #[tokio::test]
484 async fn unauthorized() {
485 let server = setup_test_server().await;
486 let backend = create_test_backend("default", "192.168.1.1:8080");
487
488 let response = server.server.post("/discovery").json(&backend).await;
489
490 assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
491
492 let response = server
493 .server
494 .get("/discovery/default/inet/MTkyLjE2OC4xLjE6ODA4MA")
495 .await;
496
497 assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
498
499 let response = server
500 .server
501 .put("/discovery/default/inet/MTkyLjE2OC4xLjE6ODA4MA")
502 .json(&backend)
503 .await;
504
505 assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
506
507 let response = server.server.delete("/discovery/default").await;
508
509 assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
510 }
511}