max / synckit
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+56 insertions,
-0 deletions
| @@ -292,3 +292,59 @@ | |||
| 292 | 292 | "Restored expired token should return TokenExpired, got: {err:?}" | |
| 293 | 293 | ); | |
| 294 | 294 | } | |
| 295 | + | ||
| 296 | + | // ── API-key validation ── | |
| 297 | + | // | |
| 298 | + | // `validate_api_key` is a free function: it builds its own HTTP client rather | |
| 299 | + | // than going through `SyncKitClient`, so nothing else in the suite covers it. | |
| 300 | + | // A setup UI calls it before saving a key, and it has to tell "wrong key" | |
| 301 | + | // (401) apart from "server unreachable" or the UI reports the wrong problem. | |
| 302 | + | ||
| 303 | + | const VALIDATE_PATH: &str = "/api/v1/sync/validate-app"; | |
| 304 | + | ||
| 305 | + | #[tokio::test] | |
| 306 | + | async fn validate_api_key_returns_the_app_name() { | |
| 307 | + | ensure_crypto_provider(); | |
| 308 | + | let kit = MockKit::start().await; | |
| 309 | + | kit.post(VALIDATE_PATH) | |
| 310 | + | .json(json!({"app_name": "goingson"})) | |
| 311 | + | .await; | |
| 312 | + | ||
| 313 | + | let name = synckit_client::validate_api_key(&kit.uri(), "sk_live_whatever") | |
| 314 | + | .await | |
| 315 | + | .expect("a 200 carrying app_name validates"); | |
| 316 | + | assert_eq!(name, "goingson"); | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | #[tokio::test] | |
| 320 | + | async fn validate_api_key_reports_401_as_a_server_error() { | |
| 321 | + | ensure_crypto_provider(); | |
| 322 | + | let kit = MockKit::start().await; | |
| 323 | + | kit.post(VALIDATE_PATH).code(401).text("nope").await; | |
| 324 | + | ||
| 325 | + | let err = synckit_client::validate_api_key(&kit.uri(), "sk_live_wrong") | |
| 326 | + | .await | |
| 327 | + | .expect_err("a rejected key is an error"); | |
| 328 | + | assert!( | |
| 329 | + | matches!(err, SyncKitError::Server { status: 401, .. }), | |
| 330 | + | "expected a 401 Server error, got {err:?}" | |
| 331 | + | ); | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | #[tokio::test] | |
| 335 | + | async fn validate_api_key_unreachable_server_is_not_the_401_variant() { | |
| 336 | + | ensure_crypto_provider(); | |
| 337 | + | // Bind then drop, so the port is closed and nothing is listening. A setup UI | |
| 338 | + | // must not tell the user their key is wrong when the server is down. | |
| 339 | + | let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind a loopback port"); | |
| 340 | + | let port = listener.local_addr().expect("the bound address").port(); | |
| 341 | + | drop(listener); | |
| 342 | + | ||
| 343 | + | let err = synckit_client::validate_api_key(&format!("http://127.0.0.1:{port}"), "sk_live_any") | |
| 344 | + | .await | |
| 345 | + | .expect_err("a closed port cannot validate anything"); | |
| 346 | + | assert!( | |
| 347 | + | !matches!(err, SyncKitError::Server { status: 401, .. }), | |
| 348 | + | "an unreachable server must not read as a rejected key, got {err:?}" | |
| 349 | + | ); | |
| 350 | + | } |