//! Domain tests for the [`Memory`] entity and its value objects (LOT A, étage 1): //! [`MemorySlug`] validation, `[[slug]]` link scanning, frontmatter serde //! round-trip, and the derived index entry. use domain::{ DomainError, MarkdownDoc, Memory, MemoryFrontmatter, MemorySlug, MemoryType, }; fn fm(slug: &str, description: &str, ty: MemoryType) -> MemoryFrontmatter { MemoryFrontmatter { name: MemorySlug::new(slug).unwrap(), description: description.to_string(), r#type: ty, } } fn note(slug: &str, body: &str) -> Memory { Memory::new( fm(slug, "a hook", MemoryType::Project), MarkdownDoc::new(body), ) .unwrap() } // --------------------------------------------------------------------------- // MemorySlug // --------------------------------------------------------------------------- #[test] fn slug_accepts_kebab_case() { assert_eq!( MemorySlug::new("my-note-42").unwrap().as_str(), "my-note-42" ); assert!(MemorySlug::new("abc").is_ok()); assert!(MemorySlug::new("a-b-c-1-2-3").is_ok()); } #[test] fn slug_rejects_uppercase() { assert!(matches!( MemorySlug::new("MyNote").unwrap_err(), DomainError::InvalidSlug { .. } )); } #[test] fn slug_rejects_spaces() { assert!(matches!( MemorySlug::new("my note").unwrap_err(), DomainError::InvalidSlug { .. } )); } #[test] fn slug_rejects_dot_and_traversal() { assert!(matches!( MemorySlug::new("..").unwrap_err(), DomainError::InvalidSlug { .. } )); assert!(matches!( MemorySlug::new("a.b").unwrap_err(), DomainError::InvalidSlug { .. } )); } #[test] fn slug_rejects_empty() { assert!(matches!( MemorySlug::new("").unwrap_err(), DomainError::InvalidSlug { .. } )); } #[test] fn slug_rejects_underscore_and_slash() { assert!(MemorySlug::new("a_b").is_err()); assert!(MemorySlug::new("a/b").is_err()); } // --------------------------------------------------------------------------- // Memory invariants // --------------------------------------------------------------------------- #[test] fn memory_rejects_empty_description() { let r = Memory::new( fm("ok-slug", "", MemoryType::User), MarkdownDoc::new("body"), ); assert!(matches!(r.unwrap_err(), DomainError::EmptyField { .. })); } #[test] fn memory_rejects_empty_body() { let r = Memory::new(fm("ok-slug", "hook", MemoryType::User), MarkdownDoc::new("")); assert!(matches!(r.unwrap_err(), DomainError::EmptyField { .. })); } // --------------------------------------------------------------------------- // outgoing_links // --------------------------------------------------------------------------- #[test] fn outgoing_links_none() { assert!(note("n", "plain body, no links").outgoing_links().is_empty()); } #[test] fn outgoing_links_multiple() { let m = note("n", "see [[alpha]] and also [[beta-2]] here"); let targets: Vec<_> = m .outgoing_links() .into_iter() .map(|l| l.target.as_str().to_string()) .collect(); assert_eq!(targets, vec!["alpha".to_string(), "beta-2".to_string()]); } #[test] fn outgoing_links_preserves_duplicates() { let m = note("n", "[[alpha]] then again [[alpha]]"); assert_eq!(m.outgoing_links().len(), 2); } #[test] fn outgoing_links_skips_invalid_targets() { // `[[Not Valid]]` is not a kebab-case slug -> skipped. let m = note("n", "[[Not Valid]] but [[good-one]]"); let targets: Vec<_> = m .outgoing_links() .into_iter() .map(|l| l.target.as_str().to_string()) .collect(); assert_eq!(targets, vec!["good-one".to_string()]); } #[test] fn outgoing_links_ignores_unterminated() { let m = note("n", "dangling [[oops without close"); assert!(m.outgoing_links().is_empty()); } // --------------------------------------------------------------------------- // index_entry // --------------------------------------------------------------------------- #[test] fn index_entry_carries_slug_hook_and_type() { let m = Memory::new( fm("topic-x", "a useful hook", MemoryType::Feedback), MarkdownDoc::new("body"), ) .unwrap(); let e = m.index_entry(); assert_eq!(e.slug.as_str(), "topic-x"); assert_eq!(e.hook, "a useful hook"); assert_eq!(e.title, "topic-x"); assert_eq!(e.r#type, MemoryType::Feedback); } // --------------------------------------------------------------------------- // Frontmatter serde round-trip (the `type` field name + camelCase variants). // --------------------------------------------------------------------------- #[test] fn frontmatter_serde_roundtrip() { let original = fm("round-trip", "desc", MemoryType::Reference); let json = serde_json::to_string(&original).unwrap(); // `type` field name (not `kind`), camelCase enum value. assert!(json.contains("\"type\":\"reference\"")); assert!(json.contains("\"name\":\"round-trip\"")); let back: MemoryFrontmatter = serde_json::from_str(&json).unwrap(); assert_eq!(back, original); } #[test] fn memory_type_serde_is_camel_case() { assert_eq!( serde_json::to_string(&MemoryType::User).unwrap(), "\"user\"" ); assert_eq!( serde_json::to_string(&MemoryType::Project).unwrap(), "\"project\"" ); }