this repo has no description
1use tranquil_pds::validation::{ 2 RecordValidator, ValidationError, ValidationStatus, validate_collection_nsid, 3 validate_record_key, 4}; 5use serde_json::json; 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!(validator.validate(&valid_post, "app.bsky.feed.post").unwrap(), ValidationStatus::Valid); 21 22 let missing_text = json!({ 23 "$type": "app.bsky.feed.post", 24 "createdAt": now() 25 }); 26 assert!(matches!(validator.validate(&missing_text, "app.bsky.feed.post"), Err(ValidationError::MissingField(f)) if f == "text")); 27 28 let missing_created_at = json!({ 29 "$type": "app.bsky.feed.post", 30 "text": "Hello" 31 }); 32 assert!(matches!(validator.validate(&missing_created_at, "app.bsky.feed.post"), Err(ValidationError::MissingField(f)) if f == "createdAt")); 33 34 let text_too_long = json!({ 35 "$type": "app.bsky.feed.post", 36 "text": "a".repeat(3001), 37 "createdAt": now() 38 }); 39 assert!(matches!(validator.validate(&text_too_long, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path == "text")); 40 41 let text_at_limit = json!({ 42 "$type": "app.bsky.feed.post", 43 "text": "a".repeat(3000), 44 "createdAt": now() 45 }); 46 assert_eq!(validator.validate(&text_at_limit, "app.bsky.feed.post").unwrap(), ValidationStatus::Valid); 47 48 let too_many_langs = json!({ 49 "$type": "app.bsky.feed.post", 50 "text": "Hello", 51 "createdAt": now(), 52 "langs": ["en", "fr", "de", "es"] 53 }); 54 assert!(matches!(validator.validate(&too_many_langs, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path == "langs")); 55 56 let three_langs_ok = json!({ 57 "$type": "app.bsky.feed.post", 58 "text": "Hello", 59 "createdAt": now(), 60 "langs": ["en", "fr", "de"] 61 }); 62 assert_eq!(validator.validate(&three_langs_ok, "app.bsky.feed.post").unwrap(), ValidationStatus::Valid); 63 64 let too_many_tags = json!({ 65 "$type": "app.bsky.feed.post", 66 "text": "Hello", 67 "createdAt": now(), 68 "tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9"] 69 }); 70 assert!(matches!(validator.validate(&too_many_tags, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path == "tags")); 71 72 let eight_tags_ok = json!({ 73 "$type": "app.bsky.feed.post", 74 "text": "Hello", 75 "createdAt": now(), 76 "tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8"] 77 }); 78 assert_eq!(validator.validate(&eight_tags_ok, "app.bsky.feed.post").unwrap(), ValidationStatus::Valid); 79 80 let tag_too_long = json!({ 81 "$type": "app.bsky.feed.post", 82 "text": "Hello", 83 "createdAt": now(), 84 "tags": ["t".repeat(641)] 85 }); 86 assert!(matches!(validator.validate(&tag_too_long, "app.bsky.feed.post"), Err(ValidationError::InvalidField { path, .. }) if path.starts_with("tags/"))); 87} 88 89#[test] 90fn test_profile_record_validation() { 91 let validator = RecordValidator::new(); 92 93 let valid = json!({ 94 "$type": "app.bsky.actor.profile", 95 "displayName": "Test User", 96 "description": "A test user profile" 97 }); 98 assert_eq!(validator.validate(&valid, "app.bsky.actor.profile").unwrap(), ValidationStatus::Valid); 99 100 let empty_ok = json!({ 101 "$type": "app.bsky.actor.profile" 102 }); 103 assert_eq!(validator.validate(&empty_ok, "app.bsky.actor.profile").unwrap(), ValidationStatus::Valid); 104 105 let displayname_too_long = json!({ 106 "$type": "app.bsky.actor.profile", 107 "displayName": "n".repeat(641) 108 }); 109 assert!(matches!(validator.validate(&displayname_too_long, "app.bsky.actor.profile"), Err(ValidationError::InvalidField { path, .. }) if path == "displayName")); 110 111 let description_too_long = json!({ 112 "$type": "app.bsky.actor.profile", 113 "description": "d".repeat(2561) 114 }); 115 assert!(matches!(validator.validate(&description_too_long, "app.bsky.actor.profile"), Err(ValidationError::InvalidField { path, .. }) if path == "description")); 116} 117 118#[test] 119fn test_like_and_repost_validation() { 120 let validator = RecordValidator::new(); 121 122 let valid_like = json!({ 123 "$type": "app.bsky.feed.like", 124 "subject": { 125 "uri": "at://did:plc:test/app.bsky.feed.post/123", 126 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 127 }, 128 "createdAt": now() 129 }); 130 assert_eq!(validator.validate(&valid_like, "app.bsky.feed.like").unwrap(), ValidationStatus::Valid); 131 132 let missing_subject = json!({ 133 "$type": "app.bsky.feed.like", 134 "createdAt": now() 135 }); 136 assert!(matches!(validator.validate(&missing_subject, "app.bsky.feed.like"), Err(ValidationError::MissingField(f)) if f == "subject")); 137 138 let missing_subject_uri = json!({ 139 "$type": "app.bsky.feed.like", 140 "subject": { 141 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 142 }, 143 "createdAt": now() 144 }); 145 assert!(matches!(validator.validate(&missing_subject_uri, "app.bsky.feed.like"), Err(ValidationError::MissingField(f)) if f.contains("uri"))); 146 147 let invalid_subject_uri = json!({ 148 "$type": "app.bsky.feed.like", 149 "subject": { 150 "uri": "https://example.com/not-at-uri", 151 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 152 }, 153 "createdAt": now() 154 }); 155 assert!(matches!(validator.validate(&invalid_subject_uri, "app.bsky.feed.like"), Err(ValidationError::InvalidField { path, .. }) if path.contains("uri"))); 156 157 let valid_repost = json!({ 158 "$type": "app.bsky.feed.repost", 159 "subject": { 160 "uri": "at://did:plc:test/app.bsky.feed.post/123", 161 "cid": "bafyreig6xxxxxyyyyyzzzzzz" 162 }, 163 "createdAt": now() 164 }); 165 assert_eq!(validator.validate(&valid_repost, "app.bsky.feed.repost").unwrap(), ValidationStatus::Valid); 166 167 let repost_missing_subject = json!({ 168 "$type": "app.bsky.feed.repost", 169 "createdAt": now() 170 }); 171 assert!(matches!(validator.validate(&repost_missing_subject, "app.bsky.feed.repost"), Err(ValidationError::MissingField(f)) if f == "subject")); 172} 173 174#[test] 175fn test_follow_and_block_validation() { 176 let validator = RecordValidator::new(); 177 178 let valid_follow = json!({ 179 "$type": "app.bsky.graph.follow", 180 "subject": "did:plc:test12345", 181 "createdAt": now() 182 }); 183 assert_eq!(validator.validate(&valid_follow, "app.bsky.graph.follow").unwrap(), ValidationStatus::Valid); 184 185 let missing_follow_subject = json!({ 186 "$type": "app.bsky.graph.follow", 187 "createdAt": now() 188 }); 189 assert!(matches!(validator.validate(&missing_follow_subject, "app.bsky.graph.follow"), Err(ValidationError::MissingField(f)) if f == "subject")); 190 191 let invalid_follow_subject = json!({ 192 "$type": "app.bsky.graph.follow", 193 "subject": "not-a-did", 194 "createdAt": now() 195 }); 196 assert!(matches!(validator.validate(&invalid_follow_subject, "app.bsky.graph.follow"), Err(ValidationError::InvalidField { path, .. }) if path == "subject")); 197 198 let valid_block = json!({ 199 "$type": "app.bsky.graph.block", 200 "subject": "did:plc:blocked123", 201 "createdAt": now() 202 }); 203 assert_eq!(validator.validate(&valid_block, "app.bsky.graph.block").unwrap(), ValidationStatus::Valid); 204 205 let invalid_block_subject = json!({ 206 "$type": "app.bsky.graph.block", 207 "subject": "not-a-did", 208 "createdAt": now() 209 }); 210 assert!(matches!(validator.validate(&invalid_block_subject, "app.bsky.graph.block"), Err(ValidationError::InvalidField { path, .. }) if path == "subject")); 211} 212 213#[test] 214fn test_list_and_graph_records_validation() { 215 let validator = RecordValidator::new(); 216 217 let valid_list = json!({ 218 "$type": "app.bsky.graph.list", 219 "name": "My List", 220 "purpose": "app.bsky.graph.defs#modlist", 221 "createdAt": now() 222 }); 223 assert_eq!(validator.validate(&valid_list, "app.bsky.graph.list").unwrap(), ValidationStatus::Valid); 224 225 let list_name_too_long = json!({ 226 "$type": "app.bsky.graph.list", 227 "name": "n".repeat(65), 228 "purpose": "app.bsky.graph.defs#modlist", 229 "createdAt": now() 230 }); 231 assert!(matches!(validator.validate(&list_name_too_long, "app.bsky.graph.list"), Err(ValidationError::InvalidField { path, .. }) if path == "name")); 232 233 let list_empty_name = json!({ 234 "$type": "app.bsky.graph.list", 235 "name": "", 236 "purpose": "app.bsky.graph.defs#modlist", 237 "createdAt": now() 238 }); 239 assert!(matches!(validator.validate(&list_empty_name, "app.bsky.graph.list"), Err(ValidationError::InvalidField { path, .. }) if path == "name")); 240 241 let valid_list_item = json!({ 242 "$type": "app.bsky.graph.listitem", 243 "subject": "did:plc:test123", 244 "list": "at://did:plc:owner/app.bsky.graph.list/mylist", 245 "createdAt": now() 246 }); 247 assert_eq!(validator.validate(&valid_list_item, "app.bsky.graph.listitem").unwrap(), ValidationStatus::Valid); 248} 249 250#[test] 251fn test_misc_record_types_validation() { 252 let validator = RecordValidator::new(); 253 254 let valid_generator = json!({ 255 "$type": "app.bsky.feed.generator", 256 "did": "did:web:example.com", 257 "displayName": "My Feed", 258 "createdAt": now() 259 }); 260 assert_eq!(validator.validate(&valid_generator, "app.bsky.feed.generator").unwrap(), ValidationStatus::Valid); 261 262 let generator_displayname_too_long = json!({ 263 "$type": "app.bsky.feed.generator", 264 "did": "did:web:example.com", 265 "displayName": "f".repeat(241), 266 "createdAt": now() 267 }); 268 assert!(matches!(validator.validate(&generator_displayname_too_long, "app.bsky.feed.generator"), Err(ValidationError::InvalidField { path, .. }) if path == "displayName")); 269 270 let valid_threadgate = json!({ 271 "$type": "app.bsky.feed.threadgate", 272 "post": "at://did:plc:test/app.bsky.feed.post/123", 273 "createdAt": now() 274 }); 275 assert_eq!(validator.validate(&valid_threadgate, "app.bsky.feed.threadgate").unwrap(), ValidationStatus::Valid); 276 277 let valid_labeler = json!({ 278 "$type": "app.bsky.labeler.service", 279 "policies": { 280 "labelValues": ["spam", "nsfw"] 281 }, 282 "createdAt": now() 283 }); 284 assert_eq!(validator.validate(&valid_labeler, "app.bsky.labeler.service").unwrap(), ValidationStatus::Valid); 285} 286 287#[test] 288fn test_type_and_format_validation() { 289 let validator = RecordValidator::new(); 290 let strict_validator = RecordValidator::new().require_lexicon(true); 291 292 let custom_record = json!({ 293 "$type": "com.custom.record", 294 "data": "test" 295 }); 296 assert_eq!(validator.validate(&custom_record, "com.custom.record").unwrap(), ValidationStatus::Unknown); 297 assert!(matches!(strict_validator.validate(&custom_record, "com.custom.record"), Err(ValidationError::UnknownType(_)))); 298 299 let type_mismatch = json!({ 300 "$type": "app.bsky.feed.like", 301 "subject": {"uri": "at://test", "cid": "bafytest"}, 302 "createdAt": now() 303 }); 304 assert!(matches!( 305 validator.validate(&type_mismatch, "app.bsky.feed.post"), 306 Err(ValidationError::TypeMismatch { expected, actual }) if expected == "app.bsky.feed.post" && actual == "app.bsky.feed.like" 307 )); 308 309 let missing_type = json!({ 310 "text": "Hello" 311 }); 312 assert!(matches!(validator.validate(&missing_type, "app.bsky.feed.post"), Err(ValidationError::MissingType))); 313 314 let not_object = json!("just a string"); 315 assert!(matches!(validator.validate(&not_object, "app.bsky.feed.post"), Err(ValidationError::InvalidRecord(_)))); 316 317 let valid_datetime = json!({ 318 "$type": "app.bsky.feed.post", 319 "text": "Test", 320 "createdAt": "2024-01-15T10:30:00.000Z" 321 }); 322 assert_eq!(validator.validate(&valid_datetime, "app.bsky.feed.post").unwrap(), ValidationStatus::Valid); 323 324 let datetime_with_offset = json!({ 325 "$type": "app.bsky.feed.post", 326 "text": "Test", 327 "createdAt": "2024-01-15T10:30:00+05:30" 328 }); 329 assert_eq!(validator.validate(&datetime_with_offset, "app.bsky.feed.post").unwrap(), ValidationStatus::Valid); 330 331 let invalid_datetime = json!({ 332 "$type": "app.bsky.feed.post", 333 "text": "Test", 334 "createdAt": "2024/01/15" 335 }); 336 assert!(matches!(validator.validate(&invalid_datetime, "app.bsky.feed.post"), Err(ValidationError::InvalidDatetime { .. }))); 337} 338 339#[test] 340fn test_record_key_validation() { 341 assert!(validate_record_key("3k2n5j2").is_ok()); 342 assert!(validate_record_key("valid-key").is_ok()); 343 assert!(validate_record_key("valid_key").is_ok()); 344 assert!(validate_record_key("valid.key").is_ok()); 345 assert!(validate_record_key("valid~key").is_ok()); 346 assert!(validate_record_key("self").is_ok()); 347 348 assert!(matches!(validate_record_key(""), Err(ValidationError::InvalidRecord(_)))); 349 350 assert!(validate_record_key(".").is_err()); 351 assert!(validate_record_key("..").is_err()); 352 353 assert!(validate_record_key("invalid/key").is_err()); 354 assert!(validate_record_key("invalid key").is_err()); 355 assert!(validate_record_key("invalid@key").is_err()); 356 assert!(validate_record_key("invalid#key").is_err()); 357 358 assert!(matches!(validate_record_key(&"k".repeat(513)), Err(ValidationError::InvalidRecord(_)))); 359 assert!(validate_record_key(&"k".repeat(512)).is_ok()); 360} 361 362#[test] 363fn test_collection_nsid_validation() { 364 assert!(validate_collection_nsid("app.bsky.feed.post").is_ok()); 365 assert!(validate_collection_nsid("com.atproto.repo.record").is_ok()); 366 assert!(validate_collection_nsid("a.b.c").is_ok()); 367 assert!(validate_collection_nsid("my-app.domain.record-type").is_ok()); 368 369 assert!(matches!(validate_collection_nsid(""), Err(ValidationError::InvalidRecord(_)))); 370 371 assert!(validate_collection_nsid("a").is_err()); 372 assert!(validate_collection_nsid("a.b").is_err()); 373 374 assert!(validate_collection_nsid("a..b.c").is_err()); 375 assert!(validate_collection_nsid(".a.b.c").is_err()); 376 assert!(validate_collection_nsid("a.b.c.").is_err()); 377 378 assert!(validate_collection_nsid("a.b.c/d").is_err()); 379 assert!(validate_collection_nsid("a.b.c_d").is_err()); 380 assert!(validate_collection_nsid("a.b.c@d").is_err()); 381}