this repo has no description
1use bspds::validation::{RecordValidator, ValidationError, ValidationStatus, validate_record_key, validate_collection_nsid};
2use serde_json::json;
3
4fn now() -> String {
5 chrono::Utc::now().to_rfc3339()
6}
7
8#[test]
9fn test_validate_post_valid() {
10 let validator = RecordValidator::new();
11 let post = json!({
12 "$type": "app.bsky.feed.post",
13 "text": "Hello world!",
14 "createdAt": now()
15 });
16 let result = validator.validate(&post, "app.bsky.feed.post");
17 assert_eq!(result.unwrap(), ValidationStatus::Valid);
18}
19
20#[test]
21fn test_validate_post_missing_text() {
22 let validator = RecordValidator::new();
23 let post = json!({
24 "$type": "app.bsky.feed.post",
25 "createdAt": now()
26 });
27 let result = validator.validate(&post, "app.bsky.feed.post");
28 assert!(matches!(result, Err(ValidationError::MissingField(f)) if f == "text"));
29}
30
31#[test]
32fn test_validate_post_missing_created_at() {
33 let validator = RecordValidator::new();
34 let post = json!({
35 "$type": "app.bsky.feed.post",
36 "text": "Hello"
37 });
38 let result = validator.validate(&post, "app.bsky.feed.post");
39 assert!(matches!(result, Err(ValidationError::MissingField(f)) if f == "createdAt"));
40}
41
42#[test]
43fn test_validate_post_text_too_long() {
44 let validator = RecordValidator::new();
45 let long_text = "a".repeat(3001);
46 let post = json!({
47 "$type": "app.bsky.feed.post",
48 "text": long_text,
49 "createdAt": now()
50 });
51 let result = validator.validate(&post, "app.bsky.feed.post");
52 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "text"));
53}
54
55#[test]
56fn test_validate_post_text_at_limit() {
57 let validator = RecordValidator::new();
58 let limit_text = "a".repeat(3000);
59 let post = json!({
60 "$type": "app.bsky.feed.post",
61 "text": limit_text,
62 "createdAt": now()
63 });
64 let result = validator.validate(&post, "app.bsky.feed.post");
65 assert_eq!(result.unwrap(), ValidationStatus::Valid);
66}
67
68#[test]
69fn test_validate_post_too_many_langs() {
70 let validator = RecordValidator::new();
71 let post = json!({
72 "$type": "app.bsky.feed.post",
73 "text": "Hello",
74 "createdAt": now(),
75 "langs": ["en", "fr", "de", "es"]
76 });
77 let result = validator.validate(&post, "app.bsky.feed.post");
78 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "langs"));
79}
80
81#[test]
82fn test_validate_post_three_langs_ok() {
83 let validator = RecordValidator::new();
84 let post = json!({
85 "$type": "app.bsky.feed.post",
86 "text": "Hello",
87 "createdAt": now(),
88 "langs": ["en", "fr", "de"]
89 });
90 let result = validator.validate(&post, "app.bsky.feed.post");
91 assert_eq!(result.unwrap(), ValidationStatus::Valid);
92}
93
94#[test]
95fn test_validate_post_too_many_tags() {
96 let validator = RecordValidator::new();
97 let post = json!({
98 "$type": "app.bsky.feed.post",
99 "text": "Hello",
100 "createdAt": now(),
101 "tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8", "tag9"]
102 });
103 let result = validator.validate(&post, "app.bsky.feed.post");
104 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "tags"));
105}
106
107#[test]
108fn test_validate_post_eight_tags_ok() {
109 let validator = RecordValidator::new();
110 let post = json!({
111 "$type": "app.bsky.feed.post",
112 "text": "Hello",
113 "createdAt": now(),
114 "tags": ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "tag8"]
115 });
116 let result = validator.validate(&post, "app.bsky.feed.post");
117 assert_eq!(result.unwrap(), ValidationStatus::Valid);
118}
119
120#[test]
121fn test_validate_post_tag_too_long() {
122 let validator = RecordValidator::new();
123 let long_tag = "t".repeat(641);
124 let post = json!({
125 "$type": "app.bsky.feed.post",
126 "text": "Hello",
127 "createdAt": now(),
128 "tags": [long_tag]
129 });
130 let result = validator.validate(&post, "app.bsky.feed.post");
131 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path.starts_with("tags/")));
132}
133
134#[test]
135fn test_validate_profile_valid() {
136 let validator = RecordValidator::new();
137 let profile = json!({
138 "$type": "app.bsky.actor.profile",
139 "displayName": "Test User",
140 "description": "A test user profile"
141 });
142 let result = validator.validate(&profile, "app.bsky.actor.profile");
143 assert_eq!(result.unwrap(), ValidationStatus::Valid);
144}
145
146#[test]
147fn test_validate_profile_empty_ok() {
148 let validator = RecordValidator::new();
149 let profile = json!({
150 "$type": "app.bsky.actor.profile"
151 });
152 let result = validator.validate(&profile, "app.bsky.actor.profile");
153 assert_eq!(result.unwrap(), ValidationStatus::Valid);
154}
155
156#[test]
157fn test_validate_profile_displayname_too_long() {
158 let validator = RecordValidator::new();
159 let long_name = "n".repeat(641);
160 let profile = json!({
161 "$type": "app.bsky.actor.profile",
162 "displayName": long_name
163 });
164 let result = validator.validate(&profile, "app.bsky.actor.profile");
165 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "displayName"));
166}
167
168#[test]
169fn test_validate_profile_description_too_long() {
170 let validator = RecordValidator::new();
171 let long_desc = "d".repeat(2561);
172 let profile = json!({
173 "$type": "app.bsky.actor.profile",
174 "description": long_desc
175 });
176 let result = validator.validate(&profile, "app.bsky.actor.profile");
177 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "description"));
178}
179
180#[test]
181fn test_validate_like_valid() {
182 let validator = RecordValidator::new();
183 let like = json!({
184 "$type": "app.bsky.feed.like",
185 "subject": {
186 "uri": "at://did:plc:test/app.bsky.feed.post/123",
187 "cid": "bafyreig6xxxxxyyyyyzzzzzz"
188 },
189 "createdAt": now()
190 });
191 let result = validator.validate(&like, "app.bsky.feed.like");
192 assert_eq!(result.unwrap(), ValidationStatus::Valid);
193}
194
195#[test]
196fn test_validate_like_missing_subject() {
197 let validator = RecordValidator::new();
198 let like = json!({
199 "$type": "app.bsky.feed.like",
200 "createdAt": now()
201 });
202 let result = validator.validate(&like, "app.bsky.feed.like");
203 assert!(matches!(result, Err(ValidationError::MissingField(f)) if f == "subject"));
204}
205
206#[test]
207fn test_validate_like_missing_subject_uri() {
208 let validator = RecordValidator::new();
209 let like = json!({
210 "$type": "app.bsky.feed.like",
211 "subject": {
212 "cid": "bafyreig6xxxxxyyyyyzzzzzz"
213 },
214 "createdAt": now()
215 });
216 let result = validator.validate(&like, "app.bsky.feed.like");
217 assert!(matches!(result, Err(ValidationError::MissingField(f)) if f.contains("uri")));
218}
219
220#[test]
221fn test_validate_like_invalid_subject_uri() {
222 let validator = RecordValidator::new();
223 let like = json!({
224 "$type": "app.bsky.feed.like",
225 "subject": {
226 "uri": "https://example.com/not-at-uri",
227 "cid": "bafyreig6xxxxxyyyyyzzzzzz"
228 },
229 "createdAt": now()
230 });
231 let result = validator.validate(&like, "app.bsky.feed.like");
232 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path.contains("uri")));
233}
234
235#[test]
236fn test_validate_repost_valid() {
237 let validator = RecordValidator::new();
238 let repost = json!({
239 "$type": "app.bsky.feed.repost",
240 "subject": {
241 "uri": "at://did:plc:test/app.bsky.feed.post/123",
242 "cid": "bafyreig6xxxxxyyyyyzzzzzz"
243 },
244 "createdAt": now()
245 });
246 let result = validator.validate(&repost, "app.bsky.feed.repost");
247 assert_eq!(result.unwrap(), ValidationStatus::Valid);
248}
249
250#[test]
251fn test_validate_repost_missing_subject() {
252 let validator = RecordValidator::new();
253 let repost = json!({
254 "$type": "app.bsky.feed.repost",
255 "createdAt": now()
256 });
257 let result = validator.validate(&repost, "app.bsky.feed.repost");
258 assert!(matches!(result, Err(ValidationError::MissingField(f)) if f == "subject"));
259}
260
261#[test]
262fn test_validate_follow_valid() {
263 let validator = RecordValidator::new();
264 let follow = json!({
265 "$type": "app.bsky.graph.follow",
266 "subject": "did:plc:test12345",
267 "createdAt": now()
268 });
269 let result = validator.validate(&follow, "app.bsky.graph.follow");
270 assert_eq!(result.unwrap(), ValidationStatus::Valid);
271}
272
273#[test]
274fn test_validate_follow_missing_subject() {
275 let validator = RecordValidator::new();
276 let follow = json!({
277 "$type": "app.bsky.graph.follow",
278 "createdAt": now()
279 });
280 let result = validator.validate(&follow, "app.bsky.graph.follow");
281 assert!(matches!(result, Err(ValidationError::MissingField(f)) if f == "subject"));
282}
283
284#[test]
285fn test_validate_follow_invalid_subject() {
286 let validator = RecordValidator::new();
287 let follow = json!({
288 "$type": "app.bsky.graph.follow",
289 "subject": "not-a-did",
290 "createdAt": now()
291 });
292 let result = validator.validate(&follow, "app.bsky.graph.follow");
293 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "subject"));
294}
295
296#[test]
297fn test_validate_block_valid() {
298 let validator = RecordValidator::new();
299 let block = json!({
300 "$type": "app.bsky.graph.block",
301 "subject": "did:plc:blocked123",
302 "createdAt": now()
303 });
304 let result = validator.validate(&block, "app.bsky.graph.block");
305 assert_eq!(result.unwrap(), ValidationStatus::Valid);
306}
307
308#[test]
309fn test_validate_block_invalid_subject() {
310 let validator = RecordValidator::new();
311 let block = json!({
312 "$type": "app.bsky.graph.block",
313 "subject": "not-a-did",
314 "createdAt": now()
315 });
316 let result = validator.validate(&block, "app.bsky.graph.block");
317 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "subject"));
318}
319
320#[test]
321fn test_validate_list_valid() {
322 let validator = RecordValidator::new();
323 let list = json!({
324 "$type": "app.bsky.graph.list",
325 "name": "My List",
326 "purpose": "app.bsky.graph.defs#modlist",
327 "createdAt": now()
328 });
329 let result = validator.validate(&list, "app.bsky.graph.list");
330 assert_eq!(result.unwrap(), ValidationStatus::Valid);
331}
332
333#[test]
334fn test_validate_list_name_too_long() {
335 let validator = RecordValidator::new();
336 let long_name = "n".repeat(65);
337 let list = json!({
338 "$type": "app.bsky.graph.list",
339 "name": long_name,
340 "purpose": "app.bsky.graph.defs#modlist",
341 "createdAt": now()
342 });
343 let result = validator.validate(&list, "app.bsky.graph.list");
344 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "name"));
345}
346
347#[test]
348fn test_validate_list_empty_name() {
349 let validator = RecordValidator::new();
350 let list = json!({
351 "$type": "app.bsky.graph.list",
352 "name": "",
353 "purpose": "app.bsky.graph.defs#modlist",
354 "createdAt": now()
355 });
356 let result = validator.validate(&list, "app.bsky.graph.list");
357 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "name"));
358}
359
360#[test]
361fn test_validate_feed_generator_valid() {
362 let validator = RecordValidator::new();
363 let generator = json!({
364 "$type": "app.bsky.feed.generator",
365 "did": "did:web:example.com",
366 "displayName": "My Feed",
367 "createdAt": now()
368 });
369 let result = validator.validate(&generator, "app.bsky.feed.generator");
370 assert_eq!(result.unwrap(), ValidationStatus::Valid);
371}
372
373#[test]
374fn test_validate_feed_generator_displayname_too_long() {
375 let validator = RecordValidator::new();
376 let long_name = "f".repeat(241);
377 let generator = json!({
378 "$type": "app.bsky.feed.generator",
379 "did": "did:web:example.com",
380 "displayName": long_name,
381 "createdAt": now()
382 });
383 let result = validator.validate(&generator, "app.bsky.feed.generator");
384 assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "displayName"));
385}
386
387#[test]
388fn test_validate_unknown_type_returns_unknown() {
389 let validator = RecordValidator::new();
390 let custom = json!({
391 "$type": "com.custom.record",
392 "data": "test"
393 });
394 let result = validator.validate(&custom, "com.custom.record");
395 assert_eq!(result.unwrap(), ValidationStatus::Unknown);
396}
397
398#[test]
399fn test_validate_unknown_type_strict_rejects() {
400 let validator = RecordValidator::new().require_lexicon(true);
401 let custom = json!({
402 "$type": "com.custom.record",
403 "data": "test"
404 });
405 let result = validator.validate(&custom, "com.custom.record");
406 assert!(matches!(result, Err(ValidationError::UnknownType(_))));
407}
408
409#[test]
410fn test_validate_type_mismatch() {
411 let validator = RecordValidator::new();
412 let record = json!({
413 "$type": "app.bsky.feed.like",
414 "subject": {"uri": "at://test", "cid": "bafytest"},
415 "createdAt": now()
416 });
417 let result = validator.validate(&record, "app.bsky.feed.post");
418 assert!(matches!(result, Err(ValidationError::TypeMismatch { expected, actual })
419 if expected == "app.bsky.feed.post" && actual == "app.bsky.feed.like"));
420}
421
422#[test]
423fn test_validate_missing_type() {
424 let validator = RecordValidator::new();
425 let record = json!({
426 "text": "Hello"
427 });
428 let result = validator.validate(&record, "app.bsky.feed.post");
429 assert!(matches!(result, Err(ValidationError::MissingType)));
430}
431
432#[test]
433fn test_validate_not_object() {
434 let validator = RecordValidator::new();
435 let record = json!("just a string");
436 let result = validator.validate(&record, "app.bsky.feed.post");
437 assert!(matches!(result, Err(ValidationError::InvalidRecord(_))));
438}
439
440#[test]
441fn test_validate_datetime_format_valid() {
442 let validator = RecordValidator::new();
443 let post = json!({
444 "$type": "app.bsky.feed.post",
445 "text": "Test",
446 "createdAt": "2024-01-15T10:30:00.000Z"
447 });
448 let result = validator.validate(&post, "app.bsky.feed.post");
449 assert_eq!(result.unwrap(), ValidationStatus::Valid);
450}
451
452#[test]
453fn test_validate_datetime_with_offset() {
454 let validator = RecordValidator::new();
455 let post = json!({
456 "$type": "app.bsky.feed.post",
457 "text": "Test",
458 "createdAt": "2024-01-15T10:30:00+05:30"
459 });
460 let result = validator.validate(&post, "app.bsky.feed.post");
461 assert_eq!(result.unwrap(), ValidationStatus::Valid);
462}
463
464#[test]
465fn test_validate_datetime_invalid_format() {
466 let validator = RecordValidator::new();
467 let post = json!({
468 "$type": "app.bsky.feed.post",
469 "text": "Test",
470 "createdAt": "2024/01/15"
471 });
472 let result = validator.validate(&post, "app.bsky.feed.post");
473 assert!(matches!(result, Err(ValidationError::InvalidDatetime { .. })));
474}
475
476#[test]
477fn test_validate_record_key_valid() {
478 assert!(validate_record_key("3k2n5j2").is_ok());
479 assert!(validate_record_key("valid-key").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("self").is_ok());
484}
485
486#[test]
487fn test_validate_record_key_empty() {
488 let result = validate_record_key("");
489 assert!(matches!(result, Err(ValidationError::InvalidRecord(_))));
490}
491
492#[test]
493fn test_validate_record_key_dot() {
494 assert!(validate_record_key(".").is_err());
495 assert!(validate_record_key("..").is_err());
496}
497
498#[test]
499fn test_validate_record_key_invalid_chars() {
500 assert!(validate_record_key("invalid/key").is_err());
501 assert!(validate_record_key("invalid key").is_err());
502 assert!(validate_record_key("invalid@key").is_err());
503 assert!(validate_record_key("invalid#key").is_err());
504}
505
506#[test]
507fn test_validate_record_key_too_long() {
508 let long_key = "k".repeat(513);
509 let result = validate_record_key(&long_key);
510 assert!(matches!(result, Err(ValidationError::InvalidRecord(_))));
511}
512
513#[test]
514fn test_validate_record_key_at_max_length() {
515 let max_key = "k".repeat(512);
516 assert!(validate_record_key(&max_key).is_ok());
517}
518
519#[test]
520fn test_validate_collection_nsid_valid() {
521 assert!(validate_collection_nsid("app.bsky.feed.post").is_ok());
522 assert!(validate_collection_nsid("com.atproto.repo.record").is_ok());
523 assert!(validate_collection_nsid("a.b.c").is_ok());
524 assert!(validate_collection_nsid("my-app.domain.record-type").is_ok());
525}
526
527#[test]
528fn test_validate_collection_nsid_empty() {
529 let result = validate_collection_nsid("");
530 assert!(matches!(result, Err(ValidationError::InvalidRecord(_))));
531}
532
533#[test]
534fn test_validate_collection_nsid_too_few_segments() {
535 assert!(validate_collection_nsid("a").is_err());
536 assert!(validate_collection_nsid("a.b").is_err());
537}
538
539#[test]
540fn test_validate_collection_nsid_empty_segment() {
541 assert!(validate_collection_nsid("a..b.c").is_err());
542 assert!(validate_collection_nsid(".a.b.c").is_err());
543 assert!(validate_collection_nsid("a.b.c.").is_err());
544}
545
546#[test]
547fn test_validate_collection_nsid_invalid_chars() {
548 assert!(validate_collection_nsid("a.b.c/d").is_err());
549 assert!(validate_collection_nsid("a.b.c_d").is_err());
550 assert!(validate_collection_nsid("a.b.c@d").is_err());
551}
552
553#[test]
554fn test_validate_threadgate() {
555 let validator = RecordValidator::new();
556 let gate = json!({
557 "$type": "app.bsky.feed.threadgate",
558 "post": "at://did:plc:test/app.bsky.feed.post/123",
559 "createdAt": now()
560 });
561 let result = validator.validate(&gate, "app.bsky.feed.threadgate");
562 assert_eq!(result.unwrap(), ValidationStatus::Valid);
563}
564
565#[test]
566fn test_validate_labeler_service() {
567 let validator = RecordValidator::new();
568 let labeler = json!({
569 "$type": "app.bsky.labeler.service",
570 "policies": {
571 "labelValues": ["spam", "nsfw"]
572 },
573 "createdAt": now()
574 });
575 let result = validator.validate(&labeler, "app.bsky.labeler.service");
576 assert_eq!(result.unwrap(), ValidationStatus::Valid);
577}
578
579#[test]
580fn test_validate_list_item() {
581 let validator = RecordValidator::new();
582 let item = json!({
583 "$type": "app.bsky.graph.listitem",
584 "subject": "did:plc:test123",
585 "list": "at://did:plc:owner/app.bsky.graph.list/mylist",
586 "createdAt": now()
587 });
588 let result = validator.validate(&item, "app.bsky.graph.listitem");
589 assert_eq!(result.unwrap(), ValidationStatus::Valid);
590}