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