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