//! SSH key management tests: CRUD, validation, ownership. use crate::harness::TestHarness; // A real ssh-ed25519 test key (not connected to anything sensitive) const TEST_KEY_ED25519: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrJSsFMsNzFqLOsNjMoVMtQ3fMM4JhPmLPWVOmBsBzq test@example.com"; // Same key without comment (normalized form) const TEST_KEY_ED25519_NORMALIZED: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrJSsFMsNzFqLOsNjMoVMtQ3fMM4JhPmLPWVOmBsBzq"; // A different ed25519 key const TEST_KEY_ED25519_2: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHUVJXBUiiMRg1vRbLRNFnb9Yj7kkFV0MmKiS3MWXRPH other@example.com"; // ── CRUD ── #[tokio::test] async fn ssh_key_crud() { let mut h = TestHarness::new().await; h.signup("alice", "alice@example.com", "password123").await; h.login("alice", "password123").await; // List: empty initially let resp = h.client.get("/api/users/me/ssh-keys").await; assert!(resp.status.is_success()); let json: serde_json::Value = resp.json(); assert_eq!(json["data"].as_array().unwrap().len(), 0); // Add a key let body = format!( "public_key={}&label=laptop", urlencoding::encode(TEST_KEY_ED25519) ); let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await; assert!( resp.status.is_success(), "Add key failed: {} {}", resp.status, resp.text ); let json: serde_json::Value = resp.json(); let key_id = json["id"].as_str().unwrap().to_string(); assert!(json["fingerprint"].as_str().unwrap().starts_with("SHA256:")); assert_eq!(json["label"].as_str().unwrap(), "laptop"); // List: now has 1 key let resp = h.client.get("/api/users/me/ssh-keys").await; assert!(resp.status.is_success()); let json: serde_json::Value = resp.json(); assert_eq!(json["data"].as_array().unwrap().len(), 1); // Delete the key let resp = h .client .delete(&format!("/api/users/me/ssh-keys/{}", key_id)) .await; assert_eq!(resp.status, 204); // List: empty again let resp = h.client.get("/api/users/me/ssh-keys").await; let json: serde_json::Value = resp.json(); assert_eq!(json["data"].as_array().unwrap().len(), 0); } // ── Duplicate fingerprint rejected ── #[tokio::test] async fn ssh_key_duplicate_fingerprint_rejected() { let mut h = TestHarness::new().await; h.signup("bob", "bob@example.com", "password123").await; h.login("bob", "password123").await; // Add the key first time let body = format!( "public_key={}&label=key1", urlencoding::encode(TEST_KEY_ED25519) ); let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await; assert!(resp.status.is_success()); // Add the same key again (same fingerprint even with different comment) let body = format!( "public_key={}&label=key2", urlencoding::encode(TEST_KEY_ED25519_NORMALIZED) ); let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await; assert!( resp.status.is_client_error(), "Duplicate key should be rejected: {} {}", resp.status, resp.text ); } // ── Invalid format rejected ── #[tokio::test] async fn ssh_key_invalid_format_rejected() { let mut h = TestHarness::new().await; h.signup("carol", "carol@example.com", "password123").await; h.login("carol", "password123").await; // Garbage input let resp = h .client .post_form( "/api/users/me/ssh-keys", "public_key=not-a-valid-key&label=test", ) .await; assert!( resp.status.is_client_error(), "Invalid key should be rejected: {} {}", resp.status, resp.text ); // Valid prefix but bad base64 let resp = h .client .post_form( "/api/users/me/ssh-keys", "public_key=ssh-ed25519+not-base64!!!&label=test", ) .await; assert!( resp.status.is_client_error(), "Bad base64 should be rejected" ); // Unsupported key type let resp = h .client .post_form( "/api/users/me/ssh-keys", "public_key=ssh-dss+AAAAB3NzaC1kc3MAAAA&label=test", ) .await; assert!( resp.status.is_client_error(), "Unsupported key type should be rejected" ); } // ── Can't delete another user's key ── #[tokio::test] async fn ssh_key_delete_other_users_key_fails() { let mut h = TestHarness::new().await; // Alice adds a key h.signup("alice2", "alice2@example.com", "password123").await; h.login("alice2", "password123").await; let body = format!( "public_key={}&label=alice-key", urlencoding::encode(TEST_KEY_ED25519) ); let resp = h .client .post_form("/api/users/me/ssh-keys", &body) .await; assert!(resp.status.is_success()); let json: serde_json::Value = resp.json(); let alice_key_id = json["id"].as_str().unwrap().to_string(); // Log out Alice, sign up and log in as Bob h.client.post_form("/logout", "").await; h.signup("bob2", "bob2@example.com", "password123").await; h.login("bob2", "password123").await; // Bob tries to delete Alice's key let resp = h .client .delete(&format!("/api/users/me/ssh-keys/{}", alice_key_id)) .await; assert_eq!( resp.status, 404, "Should not be able to delete other user's key" ); } // ── Multiple key types ── #[tokio::test] async fn ssh_key_multiple_types() { let mut h = TestHarness::new().await; h.signup("dave", "dave@example.com", "password123").await; h.login("dave", "password123").await; // Add first key let body = format!( "public_key={}&label=ed25519", urlencoding::encode(TEST_KEY_ED25519) ); let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await; assert!(resp.status.is_success(), "ed25519 key failed: {}", resp.text); // Add a different key let body = format!( "public_key={}&label=ed25519-2", urlencoding::encode(TEST_KEY_ED25519_2) ); let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await; assert!( resp.status.is_success(), "Second ed25519 key failed: {}", resp.text ); // Should have 2 keys let resp = h.client.get("/api/users/me/ssh-keys").await; let json: serde_json::Value = resp.json(); assert_eq!(json["data"].as_array().unwrap().len(), 2); } // ── Unauthenticated access rejected ── #[tokio::test] async fn ssh_key_unauthenticated_rejected() { let mut h = TestHarness::new().await; // Not logged in — should be rejected let resp = h.client.get("/api/users/me/ssh-keys").await; assert!( resp.status.is_client_error(), "Unauthenticated list should fail: {}", resp.status ); let resp = h .client .post_form( "/api/users/me/ssh-keys", "public_key=ssh-ed25519+AAAA&label=test", ) .await; assert!( resp.status.is_client_error(), "Unauthenticated add should fail" ); } // ── Empty label is valid ── #[tokio::test] async fn ssh_key_empty_label_valid() { let mut h = TestHarness::new().await; h.signup("eve", "eve@example.com", "password123").await; h.login("eve", "password123").await; let body = format!( "public_key={}", urlencoding::encode(TEST_KEY_ED25519) ); let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await; assert!( resp.status.is_success(), "Key with no label should work: {} {}", resp.status, resp.text ); let json: serde_json::Value = resp.json(); assert_eq!(json["label"].as_str().unwrap(), ""); }