Skip to main content

max / makenotwork

7.7 KB · 264 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 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGrJSsFMsNzFqLOsNjMoVMtQ3fMM4JhPmLPWVOmBsBzq";
9
10 // A different ed25519 key
11 const TEST_KEY_ED25519_2: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHUVJXBUiiMRg1vRbLRNFnb9Yj7kkFV0MmKiS3MWXRPH other@example.com";
12
13 // ── CRUD ──
14
15 #[tokio::test]
16 async fn ssh_key_crud() {
17 let mut h = TestHarness::new().await;
18 h.signup("alice", "alice@example.com", "password123").await;
19 h.login("alice", "password123").await;
20
21 // List: empty initially
22 let resp = h.client.get("/api/users/me/ssh-keys").await;
23 assert!(resp.status.is_success());
24 let json: serde_json::Value = resp.json();
25 assert_eq!(json["data"].as_array().unwrap().len(), 0);
26
27 // Add a key
28 let body = format!(
29 "public_key={}&label=laptop",
30 urlencoding::encode(TEST_KEY_ED25519)
31 );
32 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
33 assert!(
34 resp.status.is_success(),
35 "Add key failed: {} {}",
36 resp.status,
37 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!(resp.status.is_success());
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!(resp.status.is_success());
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!(
86 resp.status.is_client_error(),
87 "Duplicate key should be rejected: {} {}",
88 resp.status,
89 resp.text
90 );
91 }
92
93 // ── Invalid format rejected ──
94
95 #[tokio::test]
96 async fn ssh_key_invalid_format_rejected() {
97 let mut h = TestHarness::new().await;
98 h.signup("carol", "carol@example.com", "password123").await;
99 h.login("carol", "password123").await;
100
101 // Garbage input
102 let resp = h
103 .client
104 .post_form(
105 "/api/users/me/ssh-keys",
106 "public_key=not-a-valid-key&label=test",
107 )
108 .await;
109 assert!(
110 resp.status.is_client_error(),
111 "Invalid key should be rejected: {} {}",
112 resp.status,
113 resp.text
114 );
115
116 // Valid prefix but bad base64
117 let resp = h
118 .client
119 .post_form(
120 "/api/users/me/ssh-keys",
121 "public_key=ssh-ed25519+not-base64!!!&label=test",
122 )
123 .await;
124 assert!(
125 resp.status.is_client_error(),
126 "Bad base64 should be rejected"
127 );
128
129 // Unsupported key type
130 let resp = h
131 .client
132 .post_form(
133 "/api/users/me/ssh-keys",
134 "public_key=ssh-dss+AAAAB3NzaC1kc3MAAAA&label=test",
135 )
136 .await;
137 assert!(
138 resp.status.is_client_error(),
139 "Unsupported key type should be rejected"
140 );
141 }
142
143 // ── Can't delete another user's key ──
144
145 #[tokio::test]
146 async fn ssh_key_delete_other_users_key_fails() {
147 let mut h = TestHarness::new().await;
148
149 // Alice adds a key
150 h.signup("alice2", "alice2@example.com", "password123").await;
151 h.login("alice2", "password123").await;
152
153 let body = format!(
154 "public_key={}&label=alice-key",
155 urlencoding::encode(TEST_KEY_ED25519)
156 );
157 let resp = h
158 .client
159 .post_form("/api/users/me/ssh-keys", &body)
160 .await;
161 assert!(resp.status.is_success());
162 let json: serde_json::Value = resp.json();
163 let alice_key_id = json["id"].as_str().unwrap().to_string();
164
165 // Log out Alice, sign up and log in as Bob
166 h.client.post_form("/logout", "").await;
167 h.signup("bob2", "bob2@example.com", "password123").await;
168 h.login("bob2", "password123").await;
169
170 // Bob tries to delete Alice's key
171 let resp = h
172 .client
173 .delete(&format!("/api/users/me/ssh-keys/{}", alice_key_id))
174 .await;
175 assert_eq!(
176 resp.status, 404,
177 "Should not be able to delete other user's key"
178 );
179 }
180
181 // ── Multiple key types ──
182
183 #[tokio::test]
184 async fn ssh_key_multiple_types() {
185 let mut h = TestHarness::new().await;
186 h.signup("dave", "dave@example.com", "password123").await;
187 h.login("dave", "password123").await;
188
189 // Add first key
190 let body = format!(
191 "public_key={}&label=ed25519",
192 urlencoding::encode(TEST_KEY_ED25519)
193 );
194 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
195 assert!(resp.status.is_success(), "ed25519 key failed: {}", resp.text);
196
197 // Add a different key
198 let body = format!(
199 "public_key={}&label=ed25519-2",
200 urlencoding::encode(TEST_KEY_ED25519_2)
201 );
202 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
203 assert!(
204 resp.status.is_success(),
205 "Second ed25519 key failed: {}",
206 resp.text
207 );
208
209 // Should have 2 keys
210 let resp = h.client.get("/api/users/me/ssh-keys").await;
211 let json: serde_json::Value = resp.json();
212 assert_eq!(json["data"].as_array().unwrap().len(), 2);
213 }
214
215 // ── Unauthenticated access rejected ──
216
217 #[tokio::test]
218 async fn ssh_key_unauthenticated_rejected() {
219 let mut h = TestHarness::new().await;
220
221 // Not logged in — should be rejected
222 let resp = h.client.get("/api/users/me/ssh-keys").await;
223 assert!(
224 resp.status.is_client_error(),
225 "Unauthenticated list should fail: {}",
226 resp.status
227 );
228
229 let resp = h
230 .client
231 .post_form(
232 "/api/users/me/ssh-keys",
233 "public_key=ssh-ed25519+AAAA&label=test",
234 )
235 .await;
236 assert!(
237 resp.status.is_client_error(),
238 "Unauthenticated add should fail"
239 );
240 }
241
242 // ── Empty label is valid ──
243
244 #[tokio::test]
245 async fn ssh_key_empty_label_valid() {
246 let mut h = TestHarness::new().await;
247 h.signup("eve", "eve@example.com", "password123").await;
248 h.login("eve", "password123").await;
249
250 let body = format!(
251 "public_key={}",
252 urlencoding::encode(TEST_KEY_ED25519)
253 );
254 let resp = h.client.post_form("/api/users/me/ssh-keys", &body).await;
255 assert!(
256 resp.status.is_success(),
257 "Key with no label should work: {} {}",
258 resp.status,
259 resp.text
260 );
261 let json: serde_json::Value = resp.json();
262 assert_eq!(json["label"].as_str().unwrap(), "");
263 }
264