use crate::document::edit::{is_reserved_name, is_valid_field_name, EditError, RESERVED_NAMES};
use crate::document::sentinel::is_valid_tag_name;
use crate::document::{Card, Document};
use crate::value::QuillValue;
use crate::version::QuillReference;
use std::str::FromStr;
fn make_doc() -> Document {
Document::from_markdown("---\nQUILL: test_quill\ntitle: Hello\n---\n\nBody text.\n").unwrap()
}
fn make_doc_with_cards() -> Document {
Document::from_markdown(
"---\nQUILL: test_quill\ntitle: Hello\n---\n\nBody.\n\n---\nCARD: note\nfoo: bar\n---\n\nCard body.\n\n---\nCARD: summary\n---\n",
)
.unwrap()
}
fn qv(s: &str) -> QuillValue {
QuillValue::from_json(serde_json::json!(s))
}
fn qv_int(n: i64) -> QuillValue {
QuillValue::from_json(serde_json::json!(n))
}
#[test]
fn test_valid_field_names() {
assert!(is_valid_field_name("title"));
assert!(is_valid_field_name("my_field"));
assert!(is_valid_field_name("_private"));
assert!(is_valid_field_name("abc123"));
assert!(is_valid_field_name("a1b2c3"));
assert!(is_valid_field_name("x"));
assert!(is_valid_field_name("_"));
}
#[test]
fn test_invalid_field_names() {
assert!(!is_valid_field_name(""));
assert!(!is_valid_field_name("Title")); assert!(!is_valid_field_name("123abc")); assert!(!is_valid_field_name("my-field")); assert!(!is_valid_field_name("my field")); assert!(!is_valid_field_name("BODY")); assert!(!is_valid_field_name("CARDS")); }
#[test]
fn test_reserved_names_constant() {
assert!(RESERVED_NAMES.contains(&"BODY"));
assert!(RESERVED_NAMES.contains(&"CARDS"));
assert!(RESERVED_NAMES.contains(&"QUILL"));
assert!(RESERVED_NAMES.contains(&"CARD"));
assert_eq!(RESERVED_NAMES.len(), 4);
}
#[test]
fn test_is_reserved_name() {
assert!(is_reserved_name("BODY"));
assert!(is_reserved_name("CARDS"));
assert!(is_reserved_name("QUILL"));
assert!(is_reserved_name("CARD"));
assert!(!is_reserved_name("body")); assert!(!is_reserved_name("title"));
assert!(!is_reserved_name(""));
}
#[test]
fn test_edit_error_reserved_name() {
let mut doc = make_doc();
let result = doc.main_mut().set_field("BODY", qv("value"));
assert_eq!(result, Err(EditError::ReservedName("BODY".to_string())));
}
#[test]
fn test_edit_error_invalid_field_name() {
let mut doc = make_doc();
let result = doc.main_mut().set_field("My-Field", qv("value"));
assert_eq!(
result,
Err(EditError::InvalidFieldName("My-Field".to_string()))
);
}
#[test]
fn test_edit_error_invalid_tag_name() {
let result = Card::new("Invalid-Tag");
assert_eq!(
result,
Err(EditError::InvalidTagName("Invalid-Tag".to_string()))
);
}
#[test]
fn test_edit_error_index_out_of_range() {
let mut doc = make_doc(); let card = Card::new("note").unwrap();
let result = doc.insert_card(5, card);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: 5, len: 0 }));
}
#[test]
fn test_edit_error_display() {
assert!(EditError::ReservedName("BODY".to_string())
.to_string()
.contains("BODY"));
assert!(EditError::InvalidFieldName("Bad-Name".to_string())
.to_string()
.contains("Bad-Name"));
assert!(EditError::InvalidTagName("Bad-Tag".to_string())
.to_string()
.contains("Bad-Tag"));
assert!(EditError::IndexOutOfRange { index: 3, len: 2 }
.to_string()
.contains("3"));
}
#[test]
fn test_document_set_field_rejects_all_reserved_names() {
for &name in RESERVED_NAMES {
let mut doc = make_doc();
let result = doc.main_mut().set_field(name, qv("value"));
assert_eq!(
result,
Err(EditError::ReservedName(name.to_string())),
"expected ReservedName for '{}'",
name
);
}
}
#[test]
fn test_card_set_field_rejects_all_reserved_names() {
for &name in RESERVED_NAMES {
let mut card = Card::new("note").unwrap();
let result = card.set_field(name, qv("value"));
assert_eq!(
result,
Err(EditError::ReservedName(name.to_string())),
"expected ReservedName for '{}'",
name
);
}
}
#[test]
fn test_document_set_field_inserts() {
let mut doc = make_doc();
doc.main_mut().set_field("author", qv("Alice")).unwrap();
assert_eq!(
doc.main().frontmatter().get("author").unwrap().as_str(),
Some("Alice")
);
}
#[test]
fn test_document_set_field_updates_existing() {
let mut doc = make_doc();
doc.main_mut().set_field("title", qv("New Title")).unwrap();
assert_eq!(
doc.main().frontmatter().get("title").unwrap().as_str(),
Some("New Title")
);
}
#[test]
fn test_document_remove_field_existing() {
let mut doc = make_doc();
let removed = doc.main_mut().remove_field("title").unwrap();
assert_eq!(removed.unwrap().as_str(), Some("Hello"));
assert!(doc.main().frontmatter().get("title").is_none());
}
#[test]
fn test_document_remove_field_absent() {
let mut doc = make_doc();
let removed = doc.main_mut().remove_field("nonexistent").unwrap();
assert!(removed.is_none());
}
#[test]
fn test_document_remove_field_reserved_throws() {
let mut doc = make_doc();
for reserved in ["BODY", "CARDS", "QUILL", "CARD"] {
match doc.main_mut().remove_field(reserved) {
Err(EditError::ReservedName(name)) => assert_eq!(name, reserved),
other => panic!("expected ReservedName for {reserved}, got {other:?}"),
}
}
}
#[test]
fn test_document_remove_field_invalid_name_throws() {
let mut doc = make_doc();
match doc.main_mut().remove_field("Bad-Name") {
Err(EditError::InvalidFieldName(name)) => assert_eq!(name, "Bad-Name"),
other => panic!("expected InvalidFieldName, got {other:?}"),
}
}
#[test]
fn test_document_set_quill_ref() {
let mut doc = make_doc();
let new_ref = QuillReference::from_str("new_quill").unwrap();
doc.set_quill_ref(new_ref);
assert_eq!(doc.quill_reference().name, "new_quill");
}
#[test]
fn test_document_replace_body() {
let mut doc = make_doc();
doc.main_mut().replace_body("New body content.");
assert_eq!(doc.main().body(), "New body content.");
}
#[test]
fn test_document_push_card() {
let mut doc = make_doc();
let card = Card::new("note").unwrap();
doc.push_card(card);
assert_eq!(doc.cards().len(), 1);
assert_eq!(doc.cards()[0].tag(), "note");
}
#[test]
fn test_document_insert_card_at_zero() {
let mut doc = make_doc_with_cards(); let card = Card::new("intro").unwrap();
doc.insert_card(0, card).unwrap();
assert_eq!(doc.cards().len(), 3);
assert_eq!(doc.cards()[0].tag(), "intro");
assert_eq!(doc.cards()[1].tag(), "note");
}
#[test]
fn test_document_insert_card_at_end() {
let mut doc = make_doc_with_cards(); let len = doc.cards().len();
let card = Card::new("footer").unwrap();
doc.insert_card(len, card).unwrap();
assert_eq!(doc.cards()[len].tag(), "footer");
}
#[test]
fn test_document_insert_card_out_of_range() {
let mut doc = make_doc(); let card = Card::new("note").unwrap();
let result = doc.insert_card(1, card);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: 1, len: 0 }));
}
#[test]
fn test_document_remove_card() {
let mut doc = make_doc_with_cards(); let removed = doc.remove_card(0);
assert!(removed.is_some());
assert_eq!(removed.unwrap().tag(), "note");
assert_eq!(doc.cards().len(), 1);
assert_eq!(doc.cards()[0].tag(), "summary");
}
#[test]
fn test_document_remove_card_out_of_range() {
let mut doc = make_doc();
let removed = doc.remove_card(0);
assert!(removed.is_none());
}
#[test]
fn test_document_card_mut() {
let mut doc = make_doc_with_cards();
{
let card = doc.card_mut(0).unwrap();
card.replace_body("Updated card body.");
}
assert_eq!(doc.cards()[0].body(), "Updated card body.");
}
#[test]
fn test_document_card_mut_out_of_range() {
let mut doc = make_doc();
assert!(doc.card_mut(0).is_none());
}
#[test]
fn test_move_card_no_op_same_index() {
let mut doc = make_doc_with_cards(); let result = doc.move_card(0, 0);
assert_eq!(result, Ok(()));
assert_eq!(doc.cards()[0].tag(), "note");
assert_eq!(doc.cards()[1].tag(), "summary");
}
#[test]
fn test_move_card_last_to_first() {
let mut doc = make_doc_with_cards(); doc.move_card(1, 0).unwrap();
assert_eq!(doc.cards()[0].tag(), "summary");
assert_eq!(doc.cards()[1].tag(), "note");
}
#[test]
fn test_move_card_first_to_last() {
let mut doc = make_doc_with_cards(); let last = doc.cards().len() - 1;
doc.move_card(0, last).unwrap();
assert_eq!(doc.cards()[0].tag(), "summary");
assert_eq!(doc.cards()[last].tag(), "note");
}
#[test]
fn test_move_card_from_out_of_range() {
let mut doc = make_doc_with_cards(); let len = doc.cards().len();
let result = doc.move_card(len, 0);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: len, len }));
}
#[test]
fn test_move_card_to_out_of_range() {
let mut doc = make_doc_with_cards(); let len = doc.cards().len();
let result = doc.move_card(0, len);
assert_eq!(result, Err(EditError::IndexOutOfRange { index: len, len }));
}
#[test]
fn test_set_card_tag_renames_in_place() {
let mut doc = make_doc_with_cards(); doc.set_card_tag(0, "annotation").unwrap();
assert_eq!(doc.cards()[0].tag(), "annotation");
assert_eq!(
doc.cards()[0].frontmatter().get("foo").unwrap().as_str(),
Some("bar")
);
assert_eq!(doc.cards()[1].tag(), "summary");
}
#[test]
fn test_set_card_tag_rejects_invalid_tag() {
let mut doc = make_doc_with_cards();
for bad in ["", "Bad", "with-dash", "1leading_digit"] {
match doc.set_card_tag(0, bad) {
Err(EditError::InvalidTagName(t)) => assert_eq!(t, bad),
other => panic!("expected InvalidTagName for {bad:?}, got {other:?}"),
}
}
assert_eq!(doc.cards()[0].tag(), "note");
}
#[test]
fn test_set_card_tag_index_out_of_range() {
let mut doc = make_doc_with_cards();
let len = doc.cards().len();
let result = doc.set_card_tag(len, "annotation");
assert_eq!(result, Err(EditError::IndexOutOfRange { index: len, len }));
}
#[test]
fn test_set_card_tag_round_trips_via_markdown() {
let mut doc = make_doc_with_cards();
doc.set_card_tag(0, "annotation").unwrap();
let md = doc.to_markdown();
let reparsed = crate::Document::from_markdown(&md).unwrap();
assert_eq!(reparsed.cards()[0].tag(), "annotation");
}
#[test]
fn test_card_new_valid() {
let card = Card::new("note").unwrap();
assert_eq!(card.tag(), "note");
assert!(card.frontmatter().is_empty());
assert_eq!(card.body(), "");
}
#[test]
fn test_card_new_invalid_tag_rejected() {
for tag in ["Note", "", "my-card"] {
assert_eq!(
Card::new(tag),
Err(EditError::InvalidTagName(tag.to_string()))
);
}
}
#[test]
fn test_card_set_field_valid() {
let mut card = Card::new("note").unwrap();
card.set_field("content", qv("Some text")).unwrap();
assert_eq!(
card.frontmatter().get("content").unwrap().as_str(),
Some("Some text")
);
}
#[test]
fn test_card_set_field_invalid_name() {
let mut card = Card::new("note").unwrap();
let result = card.set_field("Content", qv("text"));
assert_eq!(
result,
Err(EditError::InvalidFieldName("Content".to_string()))
);
}
#[test]
fn test_card_remove_field_existing() {
let mut doc = make_doc_with_cards();
let card = doc.card_mut(0).unwrap();
let removed = card.remove_field("foo").unwrap();
assert_eq!(removed.unwrap().as_str(), Some("bar"));
assert!(card.frontmatter().get("foo").is_none());
}
#[test]
fn test_card_remove_field_absent() {
let mut card = Card::new("note").unwrap();
assert!(card.remove_field("nonexistent").unwrap().is_none());
}
#[test]
fn test_card_remove_field_reserved_throws() {
let mut card = Card::new("note").unwrap();
for reserved in ["BODY", "CARDS", "QUILL", "CARD"] {
match card.remove_field(reserved) {
Err(EditError::ReservedName(name)) => assert_eq!(name, reserved),
other => panic!("expected ReservedName for {reserved}, got {other:?}"),
}
}
}
#[test]
fn test_card_remove_field_invalid_name_throws() {
let mut card = Card::new("note").unwrap();
match card.remove_field("Bad-Name") {
Err(EditError::InvalidFieldName(name)) => assert_eq!(name, "Bad-Name"),
other => panic!("expected InvalidFieldName, got {other:?}"),
}
}
#[test]
fn test_card_set_body() {
let mut card = Card::new("note").unwrap();
card.replace_body("Card body text.");
assert_eq!(card.body(), "Card body text.");
}
#[test]
fn test_invariants_after_mutation_sequence() {
let mut doc = make_doc();
doc.main_mut().set_field("author", qv("Alice")).unwrap();
doc.main_mut().set_field("version", qv_int(3)).unwrap();
let c1 = Card::new("note").unwrap();
let c2 = Card::new("summary").unwrap();
let c3 = Card::new("appendix").unwrap();
doc.push_card(c1);
doc.push_card(c2);
doc.insert_card(1, c3).unwrap();
doc.card_mut(0)
.unwrap()
.set_field("text", qv("Hello"))
.unwrap();
doc.move_card(2, 0).unwrap();
doc.remove_card(1);
doc.main_mut().replace_body("Updated body.");
doc.main_mut().remove_field("version").unwrap();
for key in doc.main().frontmatter().keys() {
assert!(
!is_reserved_name(key),
"reserved key '{}' found in frontmatter",
key
);
}
for card in doc.cards() {
let tag = card.tag();
assert!(is_valid_tag_name(&tag), "invalid tag '{}' found", tag);
}
let json = doc.to_plate_json();
assert!(json.is_object());
assert_eq!(json["QUILL"].as_str(), Some("test_quill"));
assert!(json["CARDS"].is_array());
assert_eq!(json["BODY"].as_str(), Some("Updated body."));
assert_eq!(
doc.main().frontmatter().get("author").unwrap().as_str(),
Some("Alice")
);
assert!(doc.main().frontmatter().get("version").is_none());
}
#[test]
fn test_mutators_do_not_touch_warnings() {
let doc = make_doc();
let initial_warnings = doc.warnings().to_vec();
let mut doc = doc;
doc.main_mut().set_field("extra", qv("value")).unwrap();
doc.main_mut().replace_body("New body.");
let card = Card::new("new_card").unwrap();
doc.push_card(card);
assert_eq!(doc.warnings(), initial_warnings.as_slice());
}