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