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(¬_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}