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