I've been saying "PDSes seem easy enough, they're what, some CRUD to a db? I can do that in my sleep". well i'm sleeping rn so let's go
at main 16 kB view raw
1use serde_json::json; 2use tranquil_pds::validation::{ 3 RecordValidator, ValidationError, ValidationStatus, validate_collection_nsid, 4 validate_record_key, 5}; 6 7fn now() -> String { 8 chrono::Utc::now().to_rfc3339() 9} 10 11#[test] 12fn test_post_record_validation() { 13 let validator = RecordValidator::new(); 14 15 let valid_post = json!({ 16 "$type": "app.bsky.feed.post", 17 "text": "Hello world!", 18 "createdAt": now() 19 }); 20 assert_eq!( 21 validator 22 .validate(&valid_post, "app.bsky.feed.post") 23 .unwrap(), 24 ValidationStatus::Valid 25 ); 26 27 let missing_text = json!({ 28 "$type": "app.bsky.feed.post", 29 "createdAt": now() 30 }); 31 assert!( 32 matches!(validator.validate(&missing_text, "app.bsky.feed.post"), Err(ValidationError::MissingField(f)) if f == "text") 33 ); 34 35 let missing_created_at = json!({ 36 "$type": "app.bsky.feed.post", 37 "text": "Hello" 38 }); 39 assert!( 40 matches!(validator.validate(&missing_created_at, "app.bsky.feed.post"), Err(ValidationError::MissingField(f)) if f == "createdAt") 41 ); 42 43 let text_too_long = json!({ 44 "$type": "app.bsky.feed.post", 45 "text": "a".repeat(3001), 46 "createdAt": now() 47 }); 48 assert!( 49 matches!(validator.validate(&text_too_long, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path == "text") 50 ); 51 52 let text_at_limit = json!({ 53 "$type": "app.bsky.feed.post", 54 "text": "a".repeat(3000), 55 "createdAt": now() 56 }); 57 assert_eq!( 58 validator 59 .validate(&text_at_limit, "app.bsky.feed.post") 60 .unwrap(), 61 ValidationStatus::Valid 62 ); 63 64 let too_many_langs = json!({ 65 "$type": "app.bsky.feed.post", 66 "text": "Hello", 67 "createdAt": now(), 68 "langs": ["en", "fr", "de", "es"] 69 }); 70 assert!( 71 matches!(validator.validate(&too_many_langs, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path == "langs") 72 ); 73 74 let three_langs_ok = json!({ 75 "$type": "app.bsky.feed.post", 76 "text": "Hello", 77 "createdAt": now(), 78 "langs": ["en", "fr", "de"] 79 }); 80 assert_eq!( 81 validator 82 .validate(&three_langs_ok, "app.bsky.feed.post") 83 .unwrap(), 84 ValidationStatus::Valid 85 ); 86 87 let too_many_tags = json!({ 88 "$type": "app.bsky.feed.post", 89 "text": "Hello", 90 "createdAt": now(), 91 "tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9"] 92 }); 93 assert!( 94 matches!(validator.validate(&too_many_tags, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path == "tags") 95 ); 96 97 let eight_tags_ok = json!({ 98 "$type": "app.bsky.feed.post", 99 "text": "Hello", 100 "createdAt": now(), 101 "tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8"] 102 }); 103 assert_eq!( 104 validator 105 .validate(&eight_tags_ok, "app.bsky.feed.post") 106 .unwrap(), 107 ValidationStatus::Valid 108 ); 109 110 let tag_too_long = json!({ 111 "$type": "app.bsky.feed.post", 112 "text": "Hello", 113 "createdAt": now(), 114 "tags": ["t".repeat(641)] 115 }); 116 assert!( 117 matches!(validator.validate(&tag_too_long, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path.starts_with("tags/")) 118 ); 119} 120 121#[test] 122fn test_profile_record_validation() { 123 let validator = RecordValidator::new(); 124 125 let valid = json!({ 126 "$type": "app.bsky.actor.profile", 127 "displayName": "Test User", 128 "description": "A test user profile" 129 }); 130 assert_eq!( 131 validator 132 .validate(&valid, "app.bsky.actor.profile") 133 .unwrap(), 134 ValidationStatus::Valid 135 ); 136 137 let empty_ok = json!({ 138 "$type": "app.bsky.actor.profile" 139 }); 140 assert_eq!( 141 validator 142 .validate(&empty_ok, "app.bsky.actor.profile") 143 .unwrap(), 144 ValidationStatus::Valid 145 ); 146 147 let displayname_too_long = json!({ 148 "$type": "app.bsky.actor.profile", 149 "displayName": "n".repeat(641) 150 }); 151 assert!( 152 matches!(validator.validate(&displayname_too_long, "app.bsky.actor.profile"), Err(ValidationError::InvalidField { path, .. }) if path == "displayName") 153 ); 154 155 let description_too_long = json!({ 156 "$type": "app.bsky.actor.profile", 157 "description": "d".repeat(2561) 158 }); 159 assert!( 160 matches!(validator.validate(&description_too_long, "app.bsky.actor.profile"), Err(ValidationError::InvalidField { path, .. }) if path == "description") 161 ); 162} 163 164#[test] 165fn test_like_and_repost_validation() { 166 let validator = RecordValidator::new(); 167 168 let valid_like = json!({ 169 "$type": "app.bsky.feed.like", 170 "subject": { 171 "uri": "at://did:plc:test/app.bsky.feed.post/123", 172 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 173 }, 174 "createdAt": now() 175 }); 176 assert_eq!( 177 validator 178 .validate(&valid_like, "app.bsky.feed.like") 179 .unwrap(), 180 ValidationStatus::Valid 181 ); 182 183 let missing_subject = json!({ 184 "$type": "app.bsky.feed.like", 185 "createdAt": now() 186 }); 187 assert!( 188 matches!(validator.validate(&missing_subject, "app.bsky.feed.like"), Err(ValidationError::MissingField(f)) if f == "subject") 189 ); 190 191 let missing_subject_uri = json!({ 192 "$type": "app.bsky.feed.like", 193 "subject": { 194 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 195 }, 196 "createdAt": now() 197 }); 198 assert!( 199 matches!(validator.validate(&missing_subject_uri, "app.bsky.feed.like"), Err(ValidationError::MissingField(f)) if f.contains("uri")) 200 ); 201 202 let invalid_subject_uri = json!({ 203 "$type": "app.bsky.feed.like", 204 "subject": { 205 "uri": "https://example.com/not-at-uri", 206 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 207 }, 208 "createdAt": now() 209 }); 210 assert!( 211 matches!(validator.validate(&invalid_subject_uri, "app.bsky.feed.like"), Err(ValidationError::InvalidField { path, .. }) if path.contains("uri")) 212 ); 213 214 let valid_repost = json!({ 215 "$type": "app.bsky.feed.repost", 216 "subject": { 217 "uri": "at://did:plc:test/app.bsky.feed.post/123", 218 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 219 }, 220 "createdAt": now() 221 }); 222 assert_eq!( 223 validator 224 .validate(&valid_repost, "app.bsky.feed.repost") 225 .unwrap(), 226 ValidationStatus::Valid 227 ); 228 229 let repost_missing_subject = json!({ 230 "$type": "app.bsky.feed.repost", 231 "createdAt": now() 232 }); 233 assert!( 234 matches!(validator.validate(&repost_missing_subject, "app.bsky.feed.repost"), Err(ValidationError::MissingField(f)) if f == "subject") 235 ); 236} 237 238#[test] 239fn test_follow_and_block_validation() { 240 let validator = RecordValidator::new(); 241 242 let valid_follow = json!({ 243 "$type": "app.bsky.graph.follow", 244 "subject": "did:plc:test12345", 245 "createdAt": now() 246 }); 247 assert_eq!( 248 validator 249 .validate(&valid_follow, "app.bsky.graph.follow") 250 .unwrap(), 251 ValidationStatus::Valid 252 ); 253 254 let missing_follow_subject = json!({ 255 "$type": "app.bsky.graph.follow", 256 "createdAt": now() 257 }); 258 assert!( 259 matches!(validator.validate(&missing_follow_subject, "app.bsky.graph.follow"), Err(ValidationError::MissingField(f)) if f == "subject") 260 ); 261 262 let invalid_follow_subject = json!({ 263 "$type": "app.bsky.graph.follow", 264 "subject": "not-a-did", 265 "createdAt": now() 266 }); 267 assert!( 268 matches!(validator.validate(&invalid_follow_subject, "app.bsky.graph.follow"), Err(ValidationError::InvalidField { path, .. }) if path == "subject") 269 ); 270 271 let valid_block = json!({ 272 "$type": "app.bsky.graph.block", 273 "subject": "did:plc:blocked123", 274 "createdAt": now() 275 }); 276 assert_eq!( 277 validator 278 .validate(&valid_block, "app.bsky.graph.block") 279 .unwrap(), 280 ValidationStatus::Valid 281 ); 282 283 let invalid_block_subject = json!({ 284 "$type": "app.bsky.graph.block", 285 "subject": "not-a-did", 286 "createdAt": now() 287 }); 288 assert!( 289 matches!(validator.validate(&invalid_block_subject, "app.bsky.graph.block"), Err(ValidationError::InvalidField { path, .. }) if path == "subject") 290 ); 291} 292 293#[test] 294fn test_list_and_graph_records_validation() { 295 let validator = RecordValidator::new(); 296 297 let valid_list = json!({ 298 "$type": "app.bsky.graph.list", 299 "name": "My List", 300 "purpose": "app.bsky.graph.defs#modlist", 301 "createdAt": now() 302 }); 303 assert_eq!( 304 validator 305 .validate(&valid_list, "app.bsky.graph.list") 306 .unwrap(), 307 ValidationStatus::Valid 308 ); 309 310 let list_name_too_long = json!({ 311 "$type": "app.bsky.graph.list", 312 "name": "n".repeat(65), 313 "purpose": "app.bsky.graph.defs#modlist", 314 "createdAt": now() 315 }); 316 assert!( 317 matches!(validator.validate(&list_name_too_long, "app.bsky.graph.list"), Err(ValidationError::InvalidField { path, .. }) if path == "name") 318 ); 319 320 let list_empty_name = json!({ 321 "$type": "app.bsky.graph.list", 322 "name": "", 323 "purpose": "app.bsky.graph.defs#modlist", 324 "createdAt": now() 325 }); 326 assert!( 327 matches!(validator.validate(&list_empty_name, "app.bsky.graph.list"), Err(ValidationError::InvalidField { path, .. }) if path == "name") 328 ); 329 330 let valid_list_item = json!({ 331 "$type": "app.bsky.graph.listitem", 332 "subject": "did:plc:test123", 333 "list": "at://did:plc:owner/app.bsky.graph.list/mylist", 334 "createdAt": now() 335 }); 336 assert_eq!( 337 validator 338 .validate(&valid_list_item, "app.bsky.graph.listitem") 339 .unwrap(), 340 ValidationStatus::Valid 341 ); 342} 343 344#[test] 345fn test_misc_record_types_validation() { 346 let validator = RecordValidator::new(); 347 348 let valid_generator = json!({ 349 "$type": "app.bsky.feed.generator", 350 "did": "did:web:example.com", 351 "displayName": "My Feed", 352 "createdAt": now() 353 }); 354 assert_eq!( 355 validator 356 .validate(&valid_generator, "app.bsky.feed.generator") 357 .unwrap(), 358 ValidationStatus::Valid 359 ); 360 361 let generator_displayname_too_long = json!({ 362 "$type": "app.bsky.feed.generator", 363 "did": "did:web:example.com", 364 "displayName": "f".repeat(241), 365 "createdAt": now() 366 }); 367 assert!( 368 matches!(validator.validate(&generator_displayname_too_long, "app.bsky.feed.generator"), Err(ValidationError::InvalidField { path, .. }) if path == "displayName") 369 ); 370 371 let valid_threadgate = json!({ 372 "$type": "app.bsky.feed.threadgate", 373 "post": "at://did:plc:test/app.bsky.feed.post/123", 374 "createdAt": now() 375 }); 376 assert_eq!( 377 validator 378 .validate(&valid_threadgate, "app.bsky.feed.threadgate") 379 .unwrap(), 380 ValidationStatus::Valid 381 ); 382 383 let valid_labeler = json!({ 384 "$type": "app.bsky.labeler.service", 385 "policies": { 386 "labelValues": ["spam", "nsfw"] 387 }, 388 "createdAt": now() 389 }); 390 assert_eq!( 391 validator 392 .validate(&valid_labeler, "app.bsky.labeler.service") 393 .unwrap(), 394 ValidationStatus::Valid 395 ); 396} 397 398#[test] 399fn test_type_and_format_validation() { 400 let validator = RecordValidator::new(); 401 let strict_validator = RecordValidator::new().require_lexicon(true); 402 403 let custom_record = json!({ 404 "$type": "com.custom.record", 405 "data": "test" 406 }); 407 assert_eq!( 408 validator 409 .validate(&custom_record, "com.custom.record") 410 .unwrap(), 411 ValidationStatus::Unknown 412 ); 413 assert!(matches!( 414 strict_validator.validate(&custom_record, "com.custom.record"), 415 Err(ValidationError::UnknownType(_)) 416 )); 417 418 let type_mismatch = json!({ 419 "$type": "app.bsky.feed.like", 420 "subject": {"uri": "at://test", "cid": "bafytest"}, 421 "createdAt": now() 422 }); 423 assert!(matches!( 424 validator.validate(&type_mismatch, "app.bsky.feed.post"), 425 Err(ValidationError::TypeMismatch { expected, actual }) if expected == "app.bsky.feed.post" && actual == "app.bsky.feed.like" 426 )); 427 428 let missing_type = json!({ 429 "text": "Hello" 430 }); 431 assert!(matches!( 432 validator.validate(&missing_type, "app.bsky.feed.post"), 433 Err(ValidationError::MissingType) 434 )); 435 436 let not_object = json!("just a string"); 437 assert!(matches!( 438 validator.validate(&not_object, "app.bsky.feed.post"), 439 Err(ValidationError::InvalidRecord(_)) 440 )); 441 442 let valid_datetime = json!({ 443 "$type": "app.bsky.feed.post", 444 "text": "Test", 445 "createdAt": "2024-01-15T10:30:00.000Z" 446 }); 447 assert_eq!( 448 validator 449 .validate(&valid_datetime, "app.bsky.feed.post") 450 .unwrap(), 451 ValidationStatus::Valid 452 ); 453 454 let datetime_with_offset = json!({ 455 "$type": "app.bsky.feed.post", 456 "text": "Test", 457 "createdAt": "2024-01-15T10:30:00+05:30" 458 }); 459 assert_eq!( 460 validator 461 .validate(&datetime_with_offset, "app.bsky.feed.post") 462 .unwrap(), 463 ValidationStatus::Valid 464 ); 465 466 let invalid_datetime = json!({ 467 "$type": "app.bsky.feed.post", 468 "text": "Test", 469 "createdAt": "2024/01/15" 470 }); 471 assert!(matches!( 472 validator.validate(&invalid_datetime, "app.bsky.feed.post"), 473 Err(ValidationError::InvalidDatetime { .. }) 474 )); 475} 476 477#[test] 478fn test_record_key_validation() { 479 assert!(validate_record_key("3k2n5j2").is_ok()); 480 assert!(validate_record_key("valid-key").is_ok()); 481 assert!(validate_record_key("valid_key").is_ok()); 482 assert!(validate_record_key("valid.key").is_ok()); 483 assert!(validate_record_key("valid~key").is_ok()); 484 assert!(validate_record_key("self").is_ok()); 485 486 assert!(matches!( 487 validate_record_key(""), 488 Err(ValidationError::InvalidRecord(_)) 489 )); 490 491 assert!(validate_record_key(".").is_err()); 492 assert!(validate_record_key("..").is_err()); 493 494 assert!(validate_record_key("invalid/key").is_err()); 495 assert!(validate_record_key("invalid key").is_err()); 496 assert!(validate_record_key("invalid@key").is_err()); 497 assert!(validate_record_key("invalid#key").is_err()); 498 499 assert!(matches!( 500 validate_record_key(&"k".repeat(513)), 501 Err(ValidationError::InvalidRecord(_)) 502 )); 503 assert!(validate_record_key(&"k".repeat(512)).is_ok()); 504} 505 506#[test] 507fn test_collection_nsid_validation() { 508 assert!(validate_collection_nsid("app.bsky.feed.post").is_ok()); 509 assert!(validate_collection_nsid("com.atproto.repo.record").is_ok()); 510 assert!(validate_collection_nsid("a.b.c").is_ok()); 511 assert!(validate_collection_nsid("my-app.domain.record-type").is_ok()); 512 513 assert!(matches!( 514 validate_collection_nsid(""), 515 Err(ValidationError::InvalidRecord(_)) 516 )); 517 518 assert!(validate_collection_nsid("a").is_err()); 519 assert!(validate_collection_nsid("a.b").is_err()); 520 521 assert!(validate_collection_nsid("a..b.c").is_err()); 522 assert!(validate_collection_nsid(".a.b.c").is_err()); 523 assert!(validate_collection_nsid("a.b.c.").is_err()); 524 525 assert!(validate_collection_nsid("a.b.c/d").is_err()); 526 assert!(validate_collection_nsid("a.b.c_d").is_err()); 527 assert!(validate_collection_nsid("a.b.c@d").is_err()); 528}