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