this repo has no description
1use bspds::plc::{
2 PlcError, PlcOperation, PlcService, PlcValidationContext,
3 cid_for_cbor, sign_operation, signing_key_to_did_key,
4 validate_plc_operation, validate_plc_operation_for_submission,
5 verify_operation_signature,
6};
7use k256::ecdsa::SigningKey;
8use serde_json::json;
9use std::collections::HashMap;
10
11fn create_valid_operation() -> serde_json::Value {
12 let key = SigningKey::random(&mut rand::thread_rng());
13 let did_key = signing_key_to_did_key(&key);
14 let op = json!({
15 "type": "plc_operation",
16 "rotationKeys": [did_key.clone()],
17 "verificationMethods": {
18 "atproto": did_key.clone()
19 },
20 "alsoKnownAs": ["at://test.handle"],
21 "services": {
22 "atproto_pds": {
23 "type": "AtprotoPersonalDataServer",
24 "endpoint": "https://pds.example.com"
25 }
26 },
27 "prev": null
28 });
29 sign_operation(&op, &key).unwrap()
30}
31
32#[test]
33fn test_validate_plc_operation_valid() {
34 let op = create_valid_operation();
35 let result = validate_plc_operation(&op);
36 assert!(result.is_ok());
37}
38
39#[test]
40fn test_validate_plc_operation_missing_type() {
41 let op = json!({
42 "rotationKeys": [],
43 "verificationMethods": {},
44 "alsoKnownAs": [],
45 "services": {},
46 "sig": "test"
47 });
48 let result = validate_plc_operation(&op);
49 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing type")));
50}
51
52#[test]
53fn test_validate_plc_operation_invalid_type() {
54 let op = json!({
55 "type": "invalid_type",
56 "sig": "test"
57 });
58 let result = validate_plc_operation(&op);
59 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Invalid type")));
60}
61
62#[test]
63fn test_validate_plc_operation_missing_sig() {
64 let op = json!({
65 "type": "plc_operation",
66 "rotationKeys": [],
67 "verificationMethods": {},
68 "alsoKnownAs": [],
69 "services": {}
70 });
71 let result = validate_plc_operation(&op);
72 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing sig")));
73}
74
75#[test]
76fn test_validate_plc_operation_missing_rotation_keys() {
77 let op = json!({
78 "type": "plc_operation",
79 "verificationMethods": {},
80 "alsoKnownAs": [],
81 "services": {},
82 "sig": "test"
83 });
84 let result = validate_plc_operation(&op);
85 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("rotationKeys")));
86}
87
88#[test]
89fn test_validate_plc_operation_missing_verification_methods() {
90 let op = json!({
91 "type": "plc_operation",
92 "rotationKeys": [],
93 "alsoKnownAs": [],
94 "services": {},
95 "sig": "test"
96 });
97 let result = validate_plc_operation(&op);
98 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods")));
99}
100
101#[test]
102fn test_validate_plc_operation_missing_also_known_as() {
103 let op = json!({
104 "type": "plc_operation",
105 "rotationKeys": [],
106 "verificationMethods": {},
107 "services": {},
108 "sig": "test"
109 });
110 let result = validate_plc_operation(&op);
111 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("alsoKnownAs")));
112}
113
114#[test]
115fn test_validate_plc_operation_missing_services() {
116 let op = json!({
117 "type": "plc_operation",
118 "rotationKeys": [],
119 "verificationMethods": {},
120 "alsoKnownAs": [],
121 "sig": "test"
122 });
123 let result = validate_plc_operation(&op);
124 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("services")));
125}
126
127#[test]
128fn test_validate_rotation_key_required() {
129 let key = SigningKey::random(&mut rand::thread_rng());
130 let did_key = signing_key_to_did_key(&key);
131 let server_key = "did:key:zServer123";
132 let op = json!({
133 "type": "plc_operation",
134 "rotationKeys": [did_key.clone()],
135 "verificationMethods": {"atproto": did_key.clone()},
136 "alsoKnownAs": ["at://test.handle"],
137 "services": {
138 "atproto_pds": {
139 "type": "AtprotoPersonalDataServer",
140 "endpoint": "https://pds.example.com"
141 }
142 },
143 "sig": "test"
144 });
145 let ctx = PlcValidationContext {
146 server_rotation_key: server_key.to_string(),
147 expected_signing_key: did_key.clone(),
148 expected_handle: "test.handle".to_string(),
149 expected_pds_endpoint: "https://pds.example.com".to_string(),
150 };
151 let result = validate_plc_operation_for_submission(&op, &ctx);
152 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("rotation key")));
153}
154
155#[test]
156fn test_validate_signing_key_match() {
157 let key = SigningKey::random(&mut rand::thread_rng());
158 let did_key = signing_key_to_did_key(&key);
159 let wrong_key = "did:key:zWrongKey456";
160 let op = json!({
161 "type": "plc_operation",
162 "rotationKeys": [did_key.clone()],
163 "verificationMethods": {"atproto": wrong_key},
164 "alsoKnownAs": ["at://test.handle"],
165 "services": {
166 "atproto_pds": {
167 "type": "AtprotoPersonalDataServer",
168 "endpoint": "https://pds.example.com"
169 }
170 },
171 "sig": "test"
172 });
173 let ctx = PlcValidationContext {
174 server_rotation_key: did_key.clone(),
175 expected_signing_key: did_key.clone(),
176 expected_handle: "test.handle".to_string(),
177 expected_pds_endpoint: "https://pds.example.com".to_string(),
178 };
179 let result = validate_plc_operation_for_submission(&op, &ctx);
180 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("signing key")));
181}
182
183#[test]
184fn test_validate_handle_match() {
185 let key = SigningKey::random(&mut rand::thread_rng());
186 let did_key = signing_key_to_did_key(&key);
187 let op = json!({
188 "type": "plc_operation",
189 "rotationKeys": [did_key.clone()],
190 "verificationMethods": {"atproto": did_key.clone()},
191 "alsoKnownAs": ["at://wrong.handle"],
192 "services": {
193 "atproto_pds": {
194 "type": "AtprotoPersonalDataServer",
195 "endpoint": "https://pds.example.com"
196 }
197 },
198 "sig": "test"
199 });
200 let ctx = PlcValidationContext {
201 server_rotation_key: did_key.clone(),
202 expected_signing_key: did_key.clone(),
203 expected_handle: "test.handle".to_string(),
204 expected_pds_endpoint: "https://pds.example.com".to_string(),
205 };
206 let result = validate_plc_operation_for_submission(&op, &ctx);
207 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("handle")));
208}
209
210#[test]
211fn test_validate_pds_service_type() {
212 let key = SigningKey::random(&mut rand::thread_rng());
213 let did_key = signing_key_to_did_key(&key);
214 let op = json!({
215 "type": "plc_operation",
216 "rotationKeys": [did_key.clone()],
217 "verificationMethods": {"atproto": did_key.clone()},
218 "alsoKnownAs": ["at://test.handle"],
219 "services": {
220 "atproto_pds": {
221 "type": "WrongServiceType",
222 "endpoint": "https://pds.example.com"
223 }
224 },
225 "sig": "test"
226 });
227 let ctx = PlcValidationContext {
228 server_rotation_key: did_key.clone(),
229 expected_signing_key: did_key.clone(),
230 expected_handle: "test.handle".to_string(),
231 expected_pds_endpoint: "https://pds.example.com".to_string(),
232 };
233 let result = validate_plc_operation_for_submission(&op, &ctx);
234 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("type")));
235}
236
237#[test]
238fn test_validate_pds_endpoint_match() {
239 let key = SigningKey::random(&mut rand::thread_rng());
240 let did_key = signing_key_to_did_key(&key);
241 let op = json!({
242 "type": "plc_operation",
243 "rotationKeys": [did_key.clone()],
244 "verificationMethods": {"atproto": did_key.clone()},
245 "alsoKnownAs": ["at://test.handle"],
246 "services": {
247 "atproto_pds": {
248 "type": "AtprotoPersonalDataServer",
249 "endpoint": "https://wrong.endpoint.com"
250 }
251 },
252 "sig": "test"
253 });
254 let ctx = PlcValidationContext {
255 server_rotation_key: did_key.clone(),
256 expected_signing_key: did_key.clone(),
257 expected_handle: "test.handle".to_string(),
258 expected_pds_endpoint: "https://pds.example.com".to_string(),
259 };
260 let result = validate_plc_operation_for_submission(&op, &ctx);
261 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("endpoint")));
262}
263
264#[test]
265fn test_verify_signature_secp256k1() {
266 let key = SigningKey::random(&mut rand::thread_rng());
267 let did_key = signing_key_to_did_key(&key);
268 let op = json!({
269 "type": "plc_operation",
270 "rotationKeys": [did_key.clone()],
271 "verificationMethods": {},
272 "alsoKnownAs": [],
273 "services": {},
274 "prev": null
275 });
276 let signed = sign_operation(&op, &key).unwrap();
277 let rotation_keys = vec![did_key];
278 let result = verify_operation_signature(&signed, &rotation_keys);
279 assert!(result.is_ok());
280 assert!(result.unwrap());
281}
282
283#[test]
284fn test_verify_signature_wrong_key() {
285 let key = SigningKey::random(&mut rand::thread_rng());
286 let other_key = SigningKey::random(&mut rand::thread_rng());
287 let other_did_key = signing_key_to_did_key(&other_key);
288 let op = json!({
289 "type": "plc_operation",
290 "rotationKeys": [],
291 "verificationMethods": {},
292 "alsoKnownAs": [],
293 "services": {},
294 "prev": null
295 });
296 let signed = sign_operation(&op, &key).unwrap();
297 let wrong_rotation_keys = vec![other_did_key];
298 let result = verify_operation_signature(&signed, &wrong_rotation_keys);
299 assert!(result.is_ok());
300 assert!(!result.unwrap());
301}
302
303#[test]
304fn test_verify_signature_invalid_did_key_format() {
305 let key = SigningKey::random(&mut rand::thread_rng());
306 let op = json!({
307 "type": "plc_operation",
308 "rotationKeys": [],
309 "verificationMethods": {},
310 "alsoKnownAs": [],
311 "services": {},
312 "prev": null
313 });
314 let signed = sign_operation(&op, &key).unwrap();
315 let invalid_keys = vec!["not-a-did-key".to_string()];
316 let result = verify_operation_signature(&signed, &invalid_keys);
317 assert!(result.is_ok());
318 assert!(!result.unwrap());
319}
320
321#[test]
322fn test_tombstone_validation() {
323 let op = json!({
324 "type": "plc_tombstone",
325 "prev": "bafyreig6xxxxxyyyyyzzzzzz",
326 "sig": "test"
327 });
328 let result = validate_plc_operation(&op);
329 assert!(result.is_ok());
330}
331
332#[test]
333fn test_cid_for_cbor_deterministic() {
334 let value = json!({
335 "alpha": 1,
336 "beta": 2
337 });
338 let cid1 = cid_for_cbor(&value).unwrap();
339 let cid2 = cid_for_cbor(&value).unwrap();
340 assert_eq!(cid1, cid2, "CID generation should be deterministic");
341 assert!(cid1.starts_with("bafyrei"), "CID should start with bafyrei (dag-cbor + sha256)");
342}
343
344#[test]
345fn test_cid_different_for_different_data() {
346 let value1 = json!({"data": 1});
347 let value2 = json!({"data": 2});
348 let cid1 = cid_for_cbor(&value1).unwrap();
349 let cid2 = cid_for_cbor(&value2).unwrap();
350 assert_ne!(cid1, cid2, "Different data should produce different CIDs");
351}
352
353#[test]
354fn test_signing_key_to_did_key_format() {
355 let key = SigningKey::random(&mut rand::thread_rng());
356 let did_key = signing_key_to_did_key(&key);
357 assert!(did_key.starts_with("did:key:z"), "Should start with did:key:z");
358 assert!(did_key.len() > 50, "Did key should be reasonably long");
359}
360
361#[test]
362fn test_signing_key_to_did_key_unique() {
363 let key1 = SigningKey::random(&mut rand::thread_rng());
364 let key2 = SigningKey::random(&mut rand::thread_rng());
365 let did1 = signing_key_to_did_key(&key1);
366 let did2 = signing_key_to_did_key(&key2);
367 assert_ne!(did1, did2, "Different keys should produce different did:keys");
368}
369
370#[test]
371fn test_signing_key_to_did_key_consistent() {
372 let key = SigningKey::random(&mut rand::thread_rng());
373 let did1 = signing_key_to_did_key(&key);
374 let did2 = signing_key_to_did_key(&key);
375 assert_eq!(did1, did2, "Same key should produce same did:key");
376}
377
378#[test]
379fn test_sign_operation_removes_existing_sig() {
380 let key = SigningKey::random(&mut rand::thread_rng());
381 let op = json!({
382 "type": "plc_operation",
383 "rotationKeys": [],
384 "verificationMethods": {},
385 "alsoKnownAs": [],
386 "services": {},
387 "prev": null,
388 "sig": "old_signature"
389 });
390 let signed = sign_operation(&op, &key).unwrap();
391 let new_sig = signed.get("sig").and_then(|v| v.as_str()).unwrap();
392 assert_ne!(new_sig, "old_signature", "Should replace old signature");
393}
394
395#[test]
396fn test_validate_plc_operation_not_object() {
397 let result = validate_plc_operation(&json!("not an object"));
398 assert!(matches!(result, Err(PlcError::InvalidResponse(_))));
399}
400
401#[test]
402fn test_validate_for_submission_tombstone_passes() {
403 let key = SigningKey::random(&mut rand::thread_rng());
404 let did_key = signing_key_to_did_key(&key);
405 let op = json!({
406 "type": "plc_tombstone",
407 "prev": "bafyreig6xxxxxyyyyyzzzzzz",
408 "sig": "test"
409 });
410 let ctx = PlcValidationContext {
411 server_rotation_key: did_key.clone(),
412 expected_signing_key: did_key,
413 expected_handle: "test.handle".to_string(),
414 expected_pds_endpoint: "https://pds.example.com".to_string(),
415 };
416 let result = validate_plc_operation_for_submission(&op, &ctx);
417 assert!(result.is_ok(), "Tombstone should pass submission validation");
418}
419
420#[test]
421fn test_verify_signature_missing_sig() {
422 let op = json!({
423 "type": "plc_operation",
424 "rotationKeys": [],
425 "verificationMethods": {},
426 "alsoKnownAs": [],
427 "services": {}
428 });
429 let result = verify_operation_signature(&op, &[]);
430 assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("sig")));
431}
432
433#[test]
434fn test_verify_signature_invalid_base64() {
435 let op = json!({
436 "type": "plc_operation",
437 "rotationKeys": [],
438 "verificationMethods": {},
439 "alsoKnownAs": [],
440 "services": {},
441 "sig": "not-valid-base64!!!"
442 });
443 let result = verify_operation_signature(&op, &[]);
444 assert!(matches!(result, Err(PlcError::InvalidResponse(_))));
445}
446
447#[test]
448fn test_plc_operation_struct() {
449 let mut services = HashMap::new();
450 services.insert("atproto_pds".to_string(), PlcService {
451 service_type: "AtprotoPersonalDataServer".to_string(),
452 endpoint: "https://pds.example.com".to_string(),
453 });
454 let mut verification_methods = HashMap::new();
455 verification_methods.insert("atproto".to_string(), "did:key:zTest123".to_string());
456 let op = PlcOperation {
457 op_type: "plc_operation".to_string(),
458 rotation_keys: vec!["did:key:zTest123".to_string()],
459 verification_methods,
460 also_known_as: vec!["at://test.handle".to_string()],
461 services,
462 prev: None,
463 sig: Some("test".to_string()),
464 };
465 let json_value = serde_json::to_value(&op).unwrap();
466 assert_eq!(json_value["type"], "plc_operation");
467 assert!(json_value["rotationKeys"].is_array());
468}