Skip to main content

max / makenotwork

7.5 KB · 243 lines History Blame Raw
1 //! SSH key management tests: CRUD, validation, ownership.
2
3 use crate::harness::TestHarness;
4
5 // A real ssh-ed25519 test key (not connected to anything sensitive)
6 const TEST_KEY_ED25519: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrJSsFMsNzFqLOsNjMoVMtQ3fMM4JhPmLPWVOmBsBzq test@example.com";
7 // Same key without comment (normalized form)
8 const TEST_KEY_ED25519_NORMALIZED: &str =
9 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrJSsFMsNzFqLOsNjMoVMtQ3fMM4JhPmLPWVOmBsBzq";
10
11 // A different ed25519 key
12 const TEST_KEY_ED25519_2: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHUVJXBUiiMRg1vRbLRNFnb9Yj7kkFV0MmKiS3MWXRPH other@example.com";
13
14 // ── CRUD ──
15
16 #[tokio::test]
17 async fn ssh_key_crud() {
18 let mut h = TestHarness::new().await;
19 h.signup("alice", "alice@example.com", "password123").await;
20 h.login("alice", "password123").await;
21
22 // List: empty initially
23 let resp = h.client.get("/api/users/me/ssh-keys").await;
24 assert_eq!(resp.status, 200, "{}", resp.text);
25 let json: serde_json::Value = resp.json();
26 assert_eq!(json["data"].as_array().unwrap().len(), 0);
27
28 // Add a key
29 let body = format!(
30 "public_key={}&label=laptop",
31 urlencoding::encode(TEST_KEY_ED25519)
32 );
33 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
34 assert_eq!(
35 resp.status, 200,
36 "Add key failed: {} {}",
37 resp.status, resp.text
38 );
39 let json: serde_json::Value = resp.json();
40 let key_id = json["id"].as_str().unwrap().to_string();
41 assert!(json["fingerprint"].as_str().unwrap().starts_with("SHA256:"));
42 assert_eq!(json["label"].as_str().unwrap(), "laptop");
43
44 // List: now has 1 key
45 let resp = h.client.get("/api/users/me/ssh-keys").await;
46 assert_eq!(resp.status, 200, "{}", resp.text);
47 let json: serde_json::Value = resp.json();
48 assert_eq!(json["data"].as_array().unwrap().len(), 1);
49
50 // Delete the key
51 let resp = h
52 .client
53 .delete(&format!("/api/users/me/ssh-keys/{key_id}"))
54 .await;
55 assert_eq!(resp.status, 204);
56
57 // List: empty again
58 let resp = h.client.get("/api/users/me/ssh-keys").await;
59 let json: serde_json::Value = resp.json();
60 assert_eq!(json["data"].as_array().unwrap().len(), 0);
61 }
62
63 // ── Duplicate fingerprint rejected ──
64
65 #[tokio::test]
66 async fn ssh_key_duplicate_fingerprint_rejected() {
67 let mut h = TestHarness::new().await;
68 h.signup("bob", "bob@example.com", "password123").await;
69 h.login("bob", "password123").await;
70
71 // Add the key first time
72 let body = format!(
73 "public_key={}&label=key1",
74 urlencoding::encode(TEST_KEY_ED25519)
75 );
76 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
77 assert_eq!(resp.status, 200, "{}", resp.text);
78
79 // Add the same key again (same fingerprint even with different comment)
80 let body = format!(
81 "public_key={}&label=key2",
82 urlencoding::encode(TEST_KEY_ED25519_NORMALIZED)
83 );
84 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
85 assert_eq!(
86 resp.status, 422,
87 "Duplicate key should be rejected: {} {}",
88 resp.status, resp.text
89 );
90 }
91
92 // ── Invalid format rejected ──
93
94 #[tokio::test]
95 async fn ssh_key_invalid_format_rejected() {
96 let mut h = TestHarness::new().await;
97 h.signup("carol", "carol@example.com", "password123").await;
98 h.login("carol", "password123").await;
99
100 // Garbage input
101 let resp = h
102 .client
103 .post_form(
104 "/api/users/me/ssh-keys",
105 "public_key=not-a-valid-key&label=test",
106 )
107 .await;
108 assert_eq!(
109 resp.status, 422,
110 "Invalid key should be rejected: {} {}",
111 resp.status, resp.text
112 );
113
114 // Valid prefix but bad base64
115 let resp = h
116 .client
117 .post_form(
118 "/api/users/me/ssh-keys",
119 "public_key=ssh-ed25519+not-base64!!!&label=test",
120 )
121 .await;
122 assert_eq!(resp.status, 422, "Bad base64 should be rejected");
123
124 // Unsupported key type
125 let resp = h
126 .client
127 .post_form(
128 "/api/users/me/ssh-keys",
129 "public_key=ssh-dss+AAAAB3NzaC1kc3MAAAA&label=test",
130 )
131 .await;
132 assert_eq!(resp.status, 422, "Unsupported key type should be rejected");
133 }
134
135 // ── Can't delete another user's key ──
136
137 #[tokio::test]
138 async fn ssh_key_delete_other_users_key_fails() {
139 let mut h = TestHarness::new().await;
140
141 // Alice adds a key
142 h.signup("alice2", "alice2@example.com", "password123")
143 .await;
144 h.login("alice2", "password123").await;
145
146 let body = format!(
147 "public_key={}&label=alice-key",
148 urlencoding::encode(TEST_KEY_ED25519)
149 );
150 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
151 assert_eq!(resp.status, 200, "{}", resp.text);
152 let json: serde_json::Value = resp.json();
153 let alice_key_id = json["id"].as_str().unwrap().to_string();
154
155 // Log out Alice, sign up and log in as Bob
156 h.client.post_form("/logout", "").await;
157 h.signup("bob2", "bob2@example.com", "password123").await;
158 h.login("bob2", "password123").await;
159
160 // Bob tries to delete Alice's key
161 let resp = h
162 .client
163 .delete(&format!("/api/users/me/ssh-keys/{alice_key_id}"))
164 .await;
165 assert_eq!(
166 resp.status, 404,
167 "Should not be able to delete other user's key"
168 );
169 }
170
171 // ── Multiple key types ──
172
173 #[tokio::test]
174 async fn ssh_key_multiple_types() {
175 let mut h = TestHarness::new().await;
176 h.signup("dave", "dave@example.com", "password123").await;
177 h.login("dave", "password123").await;
178
179 // Add first key
180 let body = format!(
181 "public_key={}&label=ed25519",
182 urlencoding::encode(TEST_KEY_ED25519)
183 );
184 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
185 assert_eq!(resp.status, 200, "ed25519 key failed: {}", resp.text);
186
187 // Add a different key
188 let body = format!(
189 "public_key={}&label=ed25519-2",
190 urlencoding::encode(TEST_KEY_ED25519_2)
191 );
192 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
193 assert_eq!(resp.status, 200, "Second ed25519 key failed: {}", resp.text);
194
195 // Should have 2 keys
196 let resp = h.client.get("/api/users/me/ssh-keys").await;
197 let json: serde_json::Value = resp.json();
198 assert_eq!(json["data"].as_array().unwrap().len(), 2);
199 }
200
201 // ── Unauthenticated access rejected ──
202
203 #[tokio::test]
204 async fn ssh_key_unauthenticated_rejected() {
205 let mut h = TestHarness::new().await;
206
207 // Not logged in, should be rejected
208 let resp = h.client.get("/api/users/me/ssh-keys").await;
209 assert_eq!(
210 resp.status, 401,
211 "Unauthenticated list should fail: {}",
212 resp.status
213 );
214
215 let resp = h
216 .client
217 .post_form(
218 "/api/users/me/ssh-keys",
219 "public_key=ssh-ed25519+AAAA&label=test",
220 )
221 .await;
222 assert_eq!(resp.status, 403, "Unauthenticated add should fail");
223 }
224
225 // ── Empty label is valid ──
226
227 #[tokio::test]
228 async fn ssh_key_empty_label_valid() {
229 let mut h = TestHarness::new().await;
230 h.signup("eve", "eve@example.com", "password123").await;
231 h.login("eve", "password123").await;
232
233 let body = format!("public_key={}", urlencoding::encode(TEST_KEY_ED25519));
234 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
235 assert_eq!(
236 resp.status, 200,
237 "Key with no label should work: {} {}",
238 resp.status, resp.text
239 );
240 let json: serde_json::Value = resp.json();
241 assert_eq!(json["label"].as_str().unwrap(), "");
242 }
243