this repo has no description
1use serde_json::Value;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum ValidationError {
6 #[error("No $type provided")]
7 MissingType,
8 #[error("Invalid $type: expected {expected}, got {actual}")]
9 TypeMismatch { expected: String, actual: String },
10 #[error("Missing required field: {0}")]
11 MissingField(String),
12 #[error("Invalid field value at {path}: {message}")]
13 InvalidField { path: String, message: String },
14 #[error("Invalid datetime format at {path}: must be RFC-3339/ISO-8601")]
15 InvalidDatetime { path: String },
16 #[error("Invalid record: {0}")]
17 InvalidRecord(String),
18 #[error("Unknown record type: {0}")]
19 UnknownType(String),
20 #[error("Unacceptable slur in record at {path}")]
21 BannedContent { path: String },
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ValidationStatus {
26 Valid,
27 Unknown,
28 Invalid,
29}
30
31pub struct RecordValidator {
32 require_lexicon: bool,
33}
34
35impl Default for RecordValidator {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl RecordValidator {
42 pub fn new() -> Self {
43 Self {
44 require_lexicon: false,
45 }
46 }
47
48 pub fn require_lexicon(mut self, require: bool) -> Self {
49 self.require_lexicon = require;
50 self
51 }
52
53 pub fn validate(
54 &self,
55 record: &Value,
56 collection: &str,
57 ) -> Result<ValidationStatus, ValidationError> {
58 self.validate_with_rkey(record, collection, None)
59 }
60
61 pub fn validate_with_rkey(
62 &self,
63 record: &Value,
64 collection: &str,
65 rkey: Option<&str>,
66 ) -> Result<ValidationStatus, ValidationError> {
67 let obj = record.as_object().ok_or_else(|| {
68 ValidationError::InvalidRecord("Record must be an object".to_string())
69 })?;
70 let record_type = obj
71 .get("$type")
72 .and_then(|v| v.as_str())
73 .ok_or(ValidationError::MissingType)?;
74 if record_type != collection {
75 return Err(ValidationError::TypeMismatch {
76 expected: collection.to_string(),
77 actual: record_type.to_string(),
78 });
79 }
80 if let Some(created_at) = obj.get("createdAt").and_then(|v| v.as_str()) {
81 validate_datetime(created_at, "createdAt")?;
82 }
83 match record_type {
84 "app.bsky.feed.post" => self.validate_post(obj)?,
85 "app.bsky.actor.profile" => self.validate_profile(obj)?,
86 "app.bsky.feed.like" => self.validate_like(obj)?,
87 "app.bsky.feed.repost" => self.validate_repost(obj)?,
88 "app.bsky.graph.follow" => self.validate_follow(obj)?,
89 "app.bsky.graph.block" => self.validate_block(obj)?,
90 "app.bsky.graph.list" => self.validate_list(obj)?,
91 "app.bsky.graph.listitem" => self.validate_list_item(obj)?,
92 "app.bsky.feed.generator" => self.validate_feed_generator(obj, rkey)?,
93 "app.bsky.feed.threadgate" => self.validate_threadgate(obj)?,
94 "app.bsky.labeler.service" => self.validate_labeler_service(obj)?,
95 "app.bsky.graph.starterpack" => self.validate_starterpack(obj)?,
96 _ => {
97 if self.require_lexicon {
98 return Err(ValidationError::UnknownType(record_type.to_string()));
99 }
100 return Ok(ValidationStatus::Unknown);
101 }
102 }
103 Ok(ValidationStatus::Valid)
104 }
105
106 fn validate_post(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
107 if !obj.contains_key("text") {
108 return Err(ValidationError::MissingField("text".to_string()));
109 }
110 if !obj.contains_key("createdAt") {
111 return Err(ValidationError::MissingField("createdAt".to_string()));
112 }
113 if let Some(text) = obj.get("text").and_then(|v| v.as_str()) {
114 let grapheme_count = text.chars().count();
115 if grapheme_count > 3000 {
116 return Err(ValidationError::InvalidField {
117 path: "text".to_string(),
118 message: format!(
119 "Text exceeds maximum length of 3000 characters (got {})",
120 grapheme_count
121 ),
122 });
123 }
124 }
125 if let Some(langs) = obj.get("langs").and_then(|v| v.as_array())
126 && langs.len() > 3
127 {
128 return Err(ValidationError::InvalidField {
129 path: "langs".to_string(),
130 message: "Maximum 3 languages allowed".to_string(),
131 });
132 }
133 if let Some(tags) = obj.get("tags").and_then(|v| v.as_array()) {
134 if tags.len() > 8 {
135 return Err(ValidationError::InvalidField {
136 path: "tags".to_string(),
137 message: "Maximum 8 tags allowed".to_string(),
138 });
139 }
140 for (i, tag) in tags.iter().enumerate() {
141 if let Some(tag_str) = tag.as_str() {
142 if tag_str.len() > 640 {
143 return Err(ValidationError::InvalidField {
144 path: format!("tags/{}", i),
145 message: "Tag exceeds maximum length of 640 bytes".to_string(),
146 });
147 }
148 if crate::moderation::has_explicit_slur(tag_str) {
149 return Err(ValidationError::BannedContent {
150 path: format!("tags/{}", i),
151 });
152 }
153 }
154 }
155 }
156 if let Some(facets) = obj.get("facets").and_then(|v| v.as_array()) {
157 for (i, facet) in facets.iter().enumerate() {
158 if let Some(features) = facet.get("features").and_then(|v| v.as_array()) {
159 for (j, feature) in features.iter().enumerate() {
160 let is_tag = feature
161 .get("$type")
162 .and_then(|v| v.as_str())
163 .is_some_and(|t| t == "app.bsky.richtext.facet#tag");
164 if is_tag
165 && let Some(tag) = feature.get("tag").and_then(|v| v.as_str())
166 && crate::moderation::has_explicit_slur(tag)
167 {
168 return Err(ValidationError::BannedContent {
169 path: format!("facets/{}/features/{}/tag", i, j),
170 });
171 }
172 }
173 }
174 }
175 }
176 Ok(())
177 }
178
179 fn validate_profile(
180 &self,
181 obj: &serde_json::Map<String, Value>,
182 ) -> Result<(), ValidationError> {
183 if let Some(display_name) = obj.get("displayName").and_then(|v| v.as_str()) {
184 let grapheme_count = display_name.chars().count();
185 if grapheme_count > 640 {
186 return Err(ValidationError::InvalidField {
187 path: "displayName".to_string(),
188 message: format!(
189 "Display name exceeds maximum length of 640 characters (got {})",
190 grapheme_count
191 ),
192 });
193 }
194 if crate::moderation::has_explicit_slur(display_name) {
195 return Err(ValidationError::BannedContent {
196 path: "displayName".to_string(),
197 });
198 }
199 }
200 if let Some(description) = obj.get("description").and_then(|v| v.as_str()) {
201 let grapheme_count = description.chars().count();
202 if grapheme_count > 2560 {
203 return Err(ValidationError::InvalidField {
204 path: "description".to_string(),
205 message: format!(
206 "Description exceeds maximum length of 2560 characters (got {})",
207 grapheme_count
208 ),
209 });
210 }
211 if crate::moderation::has_explicit_slur(description) {
212 return Err(ValidationError::BannedContent {
213 path: "description".to_string(),
214 });
215 }
216 }
217 Ok(())
218 }
219
220 fn validate_like(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
221 if !obj.contains_key("subject") {
222 return Err(ValidationError::MissingField("subject".to_string()));
223 }
224 if !obj.contains_key("createdAt") {
225 return Err(ValidationError::MissingField("createdAt".to_string()));
226 }
227 self.validate_strong_ref(obj.get("subject"), "subject")?;
228 Ok(())
229 }
230
231 fn validate_repost(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
232 if !obj.contains_key("subject") {
233 return Err(ValidationError::MissingField("subject".to_string()));
234 }
235 if !obj.contains_key("createdAt") {
236 return Err(ValidationError::MissingField("createdAt".to_string()));
237 }
238 self.validate_strong_ref(obj.get("subject"), "subject")?;
239 Ok(())
240 }
241
242 fn validate_follow(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
243 if !obj.contains_key("subject") {
244 return Err(ValidationError::MissingField("subject".to_string()));
245 }
246 if !obj.contains_key("createdAt") {
247 return Err(ValidationError::MissingField("createdAt".to_string()));
248 }
249 if let Some(subject) = obj.get("subject").and_then(|v| v.as_str())
250 && !subject.starts_with("did:")
251 {
252 return Err(ValidationError::InvalidField {
253 path: "subject".to_string(),
254 message: "Subject must be a DID".to_string(),
255 });
256 }
257 Ok(())
258 }
259
260 fn validate_block(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
261 if !obj.contains_key("subject") {
262 return Err(ValidationError::MissingField("subject".to_string()));
263 }
264 if !obj.contains_key("createdAt") {
265 return Err(ValidationError::MissingField("createdAt".to_string()));
266 }
267 if let Some(subject) = obj.get("subject").and_then(|v| v.as_str())
268 && !subject.starts_with("did:")
269 {
270 return Err(ValidationError::InvalidField {
271 path: "subject".to_string(),
272 message: "Subject must be a DID".to_string(),
273 });
274 }
275 Ok(())
276 }
277
278 fn validate_list(&self, obj: &serde_json::Map<String, Value>) -> Result<(), ValidationError> {
279 if !obj.contains_key("name") {
280 return Err(ValidationError::MissingField("name".to_string()));
281 }
282 if !obj.contains_key("purpose") {
283 return Err(ValidationError::MissingField("purpose".to_string()));
284 }
285 if !obj.contains_key("createdAt") {
286 return Err(ValidationError::MissingField("createdAt".to_string()));
287 }
288 if let Some(name) = obj.get("name").and_then(|v| v.as_str()) {
289 if name.is_empty() || name.len() > 64 {
290 return Err(ValidationError::InvalidField {
291 path: "name".to_string(),
292 message: "Name must be 1-64 characters".to_string(),
293 });
294 }
295 if crate::moderation::has_explicit_slur(name) {
296 return Err(ValidationError::BannedContent {
297 path: "name".to_string(),
298 });
299 }
300 }
301 Ok(())
302 }
303
304 fn validate_list_item(
305 &self,
306 obj: &serde_json::Map<String, Value>,
307 ) -> Result<(), ValidationError> {
308 if !obj.contains_key("subject") {
309 return Err(ValidationError::MissingField("subject".to_string()));
310 }
311 if !obj.contains_key("list") {
312 return Err(ValidationError::MissingField("list".to_string()));
313 }
314 if !obj.contains_key("createdAt") {
315 return Err(ValidationError::MissingField("createdAt".to_string()));
316 }
317 Ok(())
318 }
319
320 fn validate_feed_generator(
321 &self,
322 obj: &serde_json::Map<String, Value>,
323 rkey: Option<&str>,
324 ) -> Result<(), ValidationError> {
325 if !obj.contains_key("did") {
326 return Err(ValidationError::MissingField("did".to_string()));
327 }
328 if !obj.contains_key("displayName") {
329 return Err(ValidationError::MissingField("displayName".to_string()));
330 }
331 if !obj.contains_key("createdAt") {
332 return Err(ValidationError::MissingField("createdAt".to_string()));
333 }
334 if let Some(rkey) = rkey
335 && crate::moderation::has_explicit_slur(rkey)
336 {
337 return Err(ValidationError::BannedContent {
338 path: "rkey".to_string(),
339 });
340 }
341 if let Some(display_name) = obj.get("displayName").and_then(|v| v.as_str()) {
342 if display_name.is_empty() || display_name.len() > 240 {
343 return Err(ValidationError::InvalidField {
344 path: "displayName".to_string(),
345 message: "displayName must be 1-240 characters".to_string(),
346 });
347 }
348 if crate::moderation::has_explicit_slur(display_name) {
349 return Err(ValidationError::BannedContent {
350 path: "displayName".to_string(),
351 });
352 }
353 }
354 Ok(())
355 }
356
357 fn validate_starterpack(
358 &self,
359 obj: &serde_json::Map<String, Value>,
360 ) -> Result<(), ValidationError> {
361 if !obj.contains_key("name") {
362 return Err(ValidationError::MissingField("name".to_string()));
363 }
364 if !obj.contains_key("createdAt") {
365 return Err(ValidationError::MissingField("createdAt".to_string()));
366 }
367 if let Some(name) = obj.get("name").and_then(|v| v.as_str()) {
368 if name.is_empty() || name.len() > 500 {
369 return Err(ValidationError::InvalidField {
370 path: "name".to_string(),
371 message: "name must be 1-500 characters".to_string(),
372 });
373 }
374 if crate::moderation::has_explicit_slur(name) {
375 return Err(ValidationError::BannedContent {
376 path: "name".to_string(),
377 });
378 }
379 }
380 if let Some(description) = obj.get("description").and_then(|v| v.as_str()) {
381 if description.len() > 3000 {
382 return Err(ValidationError::InvalidField {
383 path: "description".to_string(),
384 message: "description must be at most 3000 characters".to_string(),
385 });
386 }
387 if crate::moderation::has_explicit_slur(description) {
388 return Err(ValidationError::BannedContent {
389 path: "description".to_string(),
390 });
391 }
392 }
393 Ok(())
394 }
395
396 fn validate_threadgate(
397 &self,
398 obj: &serde_json::Map<String, Value>,
399 ) -> Result<(), ValidationError> {
400 if !obj.contains_key("post") {
401 return Err(ValidationError::MissingField("post".to_string()));
402 }
403 if !obj.contains_key("createdAt") {
404 return Err(ValidationError::MissingField("createdAt".to_string()));
405 }
406 Ok(())
407 }
408
409 fn validate_labeler_service(
410 &self,
411 obj: &serde_json::Map<String, Value>,
412 ) -> Result<(), ValidationError> {
413 if !obj.contains_key("policies") {
414 return Err(ValidationError::MissingField("policies".to_string()));
415 }
416 if !obj.contains_key("createdAt") {
417 return Err(ValidationError::MissingField("createdAt".to_string()));
418 }
419 Ok(())
420 }
421
422 fn validate_strong_ref(
423 &self,
424 value: Option<&Value>,
425 path: &str,
426 ) -> Result<(), ValidationError> {
427 let obj =
428 value
429 .and_then(|v| v.as_object())
430 .ok_or_else(|| ValidationError::InvalidField {
431 path: path.to_string(),
432 message: "Must be a strong reference object".to_string(),
433 })?;
434 if !obj.contains_key("uri") {
435 return Err(ValidationError::MissingField(format!("{}/uri", path)));
436 }
437 if !obj.contains_key("cid") {
438 return Err(ValidationError::MissingField(format!("{}/cid", path)));
439 }
440 if let Some(uri) = obj.get("uri").and_then(|v| v.as_str())
441 && !uri.starts_with("at://")
442 {
443 return Err(ValidationError::InvalidField {
444 path: format!("{}/uri", path),
445 message: "URI must be an at:// URI".to_string(),
446 });
447 }
448 Ok(())
449 }
450}
451
452fn validate_datetime(value: &str, path: &str) -> Result<(), ValidationError> {
453 if chrono::DateTime::parse_from_rfc3339(value).is_err() {
454 return Err(ValidationError::InvalidDatetime {
455 path: path.to_string(),
456 });
457 }
458 Ok(())
459}
460
461pub fn validate_record_key(rkey: &str) -> Result<(), ValidationError> {
462 if rkey.is_empty() {
463 return Err(ValidationError::InvalidRecord(
464 "Record key cannot be empty".to_string(),
465 ));
466 }
467 if rkey.len() > 512 {
468 return Err(ValidationError::InvalidRecord(
469 "Record key exceeds maximum length of 512".to_string(),
470 ));
471 }
472 if rkey == "." || rkey == ".." {
473 return Err(ValidationError::InvalidRecord(
474 "Record key cannot be '.' or '..'".to_string(),
475 ));
476 }
477 let valid_chars = rkey
478 .chars()
479 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' || c == '~');
480 if !valid_chars {
481 return Err(ValidationError::InvalidRecord(
482 "Record key contains invalid characters (must be alphanumeric, '.', '-', '_', or '~')"
483 .to_string(),
484 ));
485 }
486 Ok(())
487}
488
489pub fn is_valid_did(did: &str) -> bool {
490 if !did.starts_with("did:") {
491 return false;
492 }
493 let parts: Vec<&str> = did.splitn(3, ':').collect();
494 if parts.len() < 3 {
495 return false;
496 }
497 let method = parts[1];
498 if method.is_empty() || !method.chars().all(|c| c.is_ascii_lowercase()) {
499 return false;
500 }
501 let id = parts[2];
502 !id.is_empty()
503}
504
505pub fn validate_did(did: &str) -> Result<(), ValidationError> {
506 if !is_valid_did(did) {
507 return Err(ValidationError::InvalidField {
508 path: "did".to_string(),
509 message: "Invalid DID format".to_string(),
510 });
511 }
512 Ok(())
513}
514
515pub fn validate_collection_nsid(collection: &str) -> Result<(), ValidationError> {
516 if collection.is_empty() {
517 return Err(ValidationError::InvalidRecord(
518 "Collection NSID cannot be empty".to_string(),
519 ));
520 }
521 let parts: Vec<&str> = collection.split('.').collect();
522 if parts.len() < 3 {
523 return Err(ValidationError::InvalidRecord(
524 "Collection NSID must have at least 3 segments".to_string(),
525 ));
526 }
527 for part in &parts {
528 if part.is_empty() {
529 return Err(ValidationError::InvalidRecord(
530 "Collection NSID segments cannot be empty".to_string(),
531 ));
532 }
533 if !part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
534 return Err(ValidationError::InvalidRecord(
535 "Collection NSID segments must be alphanumeric or hyphens".to_string(),
536 ));
537 }
538 }
539 Ok(())
540}
541
542#[derive(Debug)]
543pub struct PasswordValidationError {
544 pub errors: Vec<String>,
545}
546
547impl std::fmt::Display for PasswordValidationError {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 write!(f, "{}", self.errors.join("; "))
550 }
551}
552
553impl std::error::Error for PasswordValidationError {}
554
555pub fn validate_password(password: &str) -> Result<(), PasswordValidationError> {
556 let mut errors = Vec::new();
557
558 if password.len() < 8 {
559 errors.push("Password must be at least 8 characters".to_string());
560 }
561
562 if password.len() > 256 {
563 errors.push("Password must be at most 256 characters".to_string());
564 }
565
566 if !password.chars().any(|c| c.is_ascii_lowercase()) {
567 errors.push("Password must contain at least one lowercase letter".to_string());
568 }
569
570 if !password.chars().any(|c| c.is_ascii_uppercase()) {
571 errors.push("Password must contain at least one uppercase letter".to_string());
572 }
573
574 if !password.chars().any(|c| c.is_ascii_digit()) {
575 errors.push("Password must contain at least one number".to_string());
576 }
577
578 if is_common_password(password) {
579 errors.push("Password is too common, please choose a different one".to_string());
580 }
581
582 if errors.is_empty() {
583 Ok(())
584 } else {
585 Err(PasswordValidationError { errors })
586 }
587}
588
589fn is_common_password(password: &str) -> bool {
590 const COMMON_PASSWORDS: &[&str] = &[
591 "password",
592 "Password1",
593 "Password123",
594 "Passw0rd",
595 "Passw0rd!",
596 "12345678",
597 "123456789",
598 "1234567890",
599 "qwerty123",
600 "Qwerty123",
601 "qwertyui",
602 "Qwertyui",
603 "letmein1",
604 "Letmein1",
605 "welcome1",
606 "Welcome1",
607 "admin123",
608 "Admin123",
609 "password1",
610 "Password1!",
611 "iloveyou",
612 "Iloveyou1",
613 "monkey123",
614 "Monkey123",
615 "dragon12",
616 "Dragon123",
617 "master12",
618 "Master123",
619 "login123",
620 "Login123",
621 "abc12345",
622 "Abc12345",
623 "football",
624 "Football1",
625 "baseball",
626 "Baseball1",
627 "trustno1",
628 "Trustno1",
629 "sunshine",
630 "Sunshine1",
631 "princess",
632 "Princess1",
633 "computer",
634 "Computer1",
635 "whatever",
636 "Whatever1",
637 "nintendo",
638 "Nintendo1",
639 "bluesky1",
640 "Bluesky1",
641 "Bluesky123",
642 ];
643
644 let lower = password.to_lowercase();
645 COMMON_PASSWORDS.iter().any(|p| p.to_lowercase() == lower)
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use serde_json::json;
652
653 #[test]
654 fn test_validate_post() {
655 let validator = RecordValidator::new();
656 let valid_post = json!({
657 "$type": "app.bsky.feed.post",
658 "text": "Hello, world!",
659 "createdAt": "2024-01-01T00:00:00.000Z"
660 });
661 assert_eq!(
662 validator
663 .validate(&valid_post, "app.bsky.feed.post")
664 .unwrap(),
665 ValidationStatus::Valid
666 );
667 }
668
669 #[test]
670 fn test_validate_post_missing_text() {
671 let validator = RecordValidator::new();
672 let invalid_post = json!({
673 "$type": "app.bsky.feed.post",
674 "createdAt": "2024-01-01T00:00:00.000Z"
675 });
676 assert!(
677 validator
678 .validate(&invalid_post, "app.bsky.feed.post")
679 .is_err()
680 );
681 }
682
683 #[test]
684 fn test_validate_type_mismatch() {
685 let validator = RecordValidator::new();
686 let record = json!({
687 "$type": "app.bsky.feed.like",
688 "subject": {"uri": "at://did:plc:test/app.bsky.feed.post/123", "cid": "bafyrei..."},
689 "createdAt": "2024-01-01T00:00:00.000Z"
690 });
691 let result = validator.validate(&record, "app.bsky.feed.post");
692 assert!(matches!(result, Err(ValidationError::TypeMismatch { .. })));
693 }
694
695 #[test]
696 fn test_validate_unknown_type() {
697 let validator = RecordValidator::new();
698 let record = json!({
699 "$type": "com.example.custom",
700 "data": "test"
701 });
702 assert_eq!(
703 validator.validate(&record, "com.example.custom").unwrap(),
704 ValidationStatus::Unknown
705 );
706 }
707
708 #[test]
709 fn test_validate_unknown_type_strict() {
710 let validator = RecordValidator::new().require_lexicon(true);
711 let record = json!({
712 "$type": "com.example.custom",
713 "data": "test"
714 });
715 let result = validator.validate(&record, "com.example.custom");
716 assert!(matches!(result, Err(ValidationError::UnknownType(_))));
717 }
718
719 #[test]
720 fn test_validate_record_key() {
721 assert!(validate_record_key("valid-key_123").is_ok());
722 assert!(validate_record_key("3k2n5j2").is_ok());
723 assert!(validate_record_key(".").is_err());
724 assert!(validate_record_key("..").is_err());
725 assert!(validate_record_key("").is_err());
726 assert!(validate_record_key("invalid/key").is_err());
727 }
728
729 #[test]
730 fn test_validate_collection_nsid() {
731 assert!(validate_collection_nsid("app.bsky.feed.post").is_ok());
732 assert!(validate_collection_nsid("com.atproto.repo.record").is_ok());
733 assert!(validate_collection_nsid("invalid").is_err());
734 assert!(validate_collection_nsid("a.b").is_err());
735 assert!(validate_collection_nsid("").is_err());
736 }
737
738 #[test]
739 fn test_is_valid_did() {
740 assert!(is_valid_did("did:plc:1234567890abcdefghijk"));
741 assert!(is_valid_did("did:web:example.com"));
742 assert!(is_valid_did(
743 "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
744 ));
745 assert!(!is_valid_did(""));
746 assert!(!is_valid_did("plc:1234567890abcdefghijk"));
747 assert!(!is_valid_did("did:"));
748 assert!(!is_valid_did("did:plc:"));
749 assert!(!is_valid_did("did::something"));
750 assert!(!is_valid_did("DID:plc:test"));
751 }
752}