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