chore: source formatting and spec allignment
This commit is contained in:
@@ -25,7 +25,15 @@ fn read_project() {
|
||||
.query_row(
|
||||
"SELECT id, name, slug, data_path, is_active FROM projects WHERE id = ?1",
|
||||
[PROJECT_ID],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)),
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -52,7 +60,10 @@ fn read_published_post_has_null_content() {
|
||||
assert_eq!(title, "Esmeralda");
|
||||
assert_eq!(slug, "esmeralda");
|
||||
assert_eq!(status, "published");
|
||||
assert!(content.is_none(), "published posts must have NULL content in DB");
|
||||
assert!(
|
||||
content.is_none(),
|
||||
"published posts must have NULL content in DB"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,8 +107,14 @@ fn published_posts_have_file_paths() {
|
||||
|
||||
assert_eq!(rows.len(), 3);
|
||||
for (slug, path) in &rows {
|
||||
assert!(!path.is_empty(), "published post '{slug}' must have a file_path");
|
||||
assert!(path.ends_with(&format!("{slug}.md")), "file_path must end with {slug}.md");
|
||||
assert!(
|
||||
!path.is_empty(),
|
||||
"published post '{slug}' must have a file_path"
|
||||
);
|
||||
assert!(
|
||||
path.ends_with(&format!("{slug}.md")),
|
||||
"file_path must end with {slug}.md"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +148,14 @@ fn post_timestamps_are_unix_integers() {
|
||||
.unwrap();
|
||||
|
||||
// Sanity: timestamps should be in reasonable Unix range (year 2000+)
|
||||
assert!(created_at > 946_684_800, "created_at should be after year 2000");
|
||||
assert!(updated_at > 946_684_800, "updated_at should be after year 2000");
|
||||
assert!(
|
||||
created_at > 946_684_800,
|
||||
"created_at should be after year 2000"
|
||||
);
|
||||
assert!(
|
||||
updated_at > 946_684_800,
|
||||
"updated_at should be after year 2000"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -147,7 +170,10 @@ fn post_unique_constraint_on_project_slug() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(dupes.is_empty(), "found duplicate (project_id, slug) pairs: {dupes:?}");
|
||||
assert!(
|
||||
dupes.is_empty(),
|
||||
"found duplicate (project_id, slug) pairs: {dupes:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Post Translations ───────────────────────────────────────────────
|
||||
@@ -156,7 +182,9 @@ fn post_unique_constraint_on_project_slug() {
|
||||
fn read_post_translations() {
|
||||
let conn = fixture_db();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM post_translations", [], |row| row.get(0))
|
||||
.query_row("SELECT COUNT(*) FROM post_translations", [], |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(count, 4);
|
||||
}
|
||||
@@ -177,7 +205,10 @@ fn translation_references_valid_post() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(orphans.is_empty(), "orphan translations referencing missing posts: {orphans:?}");
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"orphan translations referencing missing posts: {orphans:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -193,7 +224,10 @@ fn published_translations_have_null_content() {
|
||||
.collect();
|
||||
|
||||
for (id, content) in &rows {
|
||||
assert!(content.is_none(), "published translation {id} must have NULL content");
|
||||
assert!(
|
||||
content.is_none(),
|
||||
"published translation {id} must have NULL content"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +327,16 @@ fn tag_names_are_expected() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert_eq!(names, vec!["fotografie", "mac-os-x", "natur", "programmierung", "sysadmin"]);
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"fotografie",
|
||||
"mac-os-x",
|
||||
"natur",
|
||||
"programmierung",
|
||||
"sysadmin"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -308,7 +351,10 @@ fn tag_unique_constraint_on_project_name() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(dupes.is_empty(), "found duplicate (project_id, name) tag pairs: {dupes:?}");
|
||||
assert!(
|
||||
dupes.is_empty(),
|
||||
"found duplicate (project_id, name) tag pairs: {dupes:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Templates ───────────────────────────────────────────────────────
|
||||
@@ -345,7 +391,10 @@ fn read_template() {
|
||||
assert_eq!(kind, "post");
|
||||
assert!(enabled);
|
||||
assert_eq!(status, "published");
|
||||
assert!(content.is_none(), "published template content should be NULL in DB");
|
||||
assert!(
|
||||
content.is_none(),
|
||||
"published template content should be NULL in DB"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Scripts ─────────────────────────────────────────────────────────
|
||||
@@ -420,7 +469,10 @@ fn settings_are_key_value_pairs() {
|
||||
assert!(!value.is_empty());
|
||||
}
|
||||
// All setting keys should contain the project ID (namespaced)
|
||||
let project_keys: Vec<_> = pairs.iter().filter(|(k, _)| k.contains(PROJECT_ID)).collect();
|
||||
let project_keys: Vec<_> = pairs
|
||||
.iter()
|
||||
.filter(|(k, _)| k.contains(PROJECT_ID))
|
||||
.collect();
|
||||
assert_eq!(project_keys.len(), 5);
|
||||
}
|
||||
|
||||
@@ -479,7 +531,10 @@ fn all_posts_belong_to_existing_project() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(orphans.is_empty(), "posts referencing missing projects: {orphans:?}");
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"posts referencing missing projects: {orphans:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -498,7 +553,10 @@ fn all_tags_belong_to_existing_project() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(orphans.is_empty(), "tags referencing missing projects: {orphans:?}");
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"tags referencing missing projects: {orphans:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -517,7 +575,10 @@ fn all_media_belong_to_existing_project() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(orphans.is_empty(), "media referencing missing projects: {orphans:?}");
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"media referencing missing projects: {orphans:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -537,7 +598,10 @@ fn post_links_reference_valid_posts() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(orphans.is_empty(), "post_links with invalid references: {orphans:?}");
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"post_links with invalid references: {orphans:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -557,5 +621,8 @@ fn post_media_references_valid_entities() {
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(orphans.is_empty(), "post_media with invalid references: {orphans:?}");
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"post_media with invalid references: {orphans:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
//! - Step 20: Golden-file comparisons against fixture files
|
||||
//! - Step 21: Metadata diff coverage matrix (every diffable field covered)
|
||||
|
||||
use bds_core::db::Database;
|
||||
use bds_core::db::fts::ensure_fts_tables;
|
||||
use bds_core::db::queries::project::insert_project;
|
||||
use bds_core::db::queries::script as qs;
|
||||
use bds_core::db::queries::template as qtpl;
|
||||
use bds_core::db::Database;
|
||||
use bds_core::engine::media;
|
||||
use bds_core::engine::meta;
|
||||
use bds_core::engine::metadata_diff;
|
||||
use bds_core::engine::post;
|
||||
use bds_core::engine::tag;
|
||||
use bds_core::model::{
|
||||
PostStatus, Project, Script, ScriptKind, ScriptStatus, Template, TemplateKind,
|
||||
TemplateStatus,
|
||||
PostStatus, Project, Script, ScriptKind, ScriptStatus, Template, TemplateKind, TemplateStatus,
|
||||
};
|
||||
use bds_core::util::frontmatter::{
|
||||
read_post_file, read_template_file, read_translation_file,
|
||||
write_script_file, write_template_file, ScriptFrontmatter, TemplateFrontmatter,
|
||||
ScriptFrontmatter, TemplateFrontmatter, read_post_file, read_template_file,
|
||||
read_translation_file, write_script_file, write_template_file,
|
||||
};
|
||||
use bds_core::util::sidecar::{read_sidecar, read_translation_sidecar};
|
||||
use image::DynamicImage;
|
||||
@@ -325,10 +324,7 @@ fn test_golden_post_esmeralda() {
|
||||
let actual = bds_core::util::frontmatter::format_frontmatter(&yaml, &body);
|
||||
|
||||
// Compare byte-for-byte with fixture
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"golden output mismatch for esmeralda.md"
|
||||
);
|
||||
assert_eq!(actual, expected, "golden output mismatch for esmeralda.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -353,8 +349,7 @@ fn test_golden_translation_esmeralda_en() {
|
||||
|
||||
#[test]
|
||||
fn test_golden_sidecar() {
|
||||
let path =
|
||||
fixture_dir().join("media/2005/11/eb0cf9d7-6fbd-4b74-9be3-759d6e16f240.jpg.meta");
|
||||
let path = fixture_dir().join("media/2005/11/eb0cf9d7-6fbd-4b74-9be3-759d6e16f240.jpg.meta");
|
||||
let expected = fs::read_to_string(&path).unwrap();
|
||||
let sc = read_sidecar(&expected).unwrap();
|
||||
|
||||
@@ -401,7 +396,14 @@ fn test_golden_meta_files() {
|
||||
assert_eq!(
|
||||
categories,
|
||||
vec![
|
||||
"article", "aside", "kochbuch", "metaowl", "page", "picture", "spielelog", "wiki"
|
||||
"article",
|
||||
"aside",
|
||||
"kochbuch",
|
||||
"metaowl",
|
||||
"page",
|
||||
"picture",
|
||||
"spielelog",
|
||||
"wiki"
|
||||
]
|
||||
);
|
||||
// Verify they are sorted
|
||||
@@ -416,7 +418,7 @@ fn test_golden_meta_files() {
|
||||
// Verify sorted by name
|
||||
let names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
|
||||
let mut sorted_names = names.clone();
|
||||
sorted_names.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
|
||||
sorted_names.sort_by_key(|a| a.to_lowercase());
|
||||
assert_eq!(names, sorted_names, "tags should be sorted by name");
|
||||
// Spot-check a known tag
|
||||
assert!(tags.iter().any(|t| t.name == "rust"));
|
||||
@@ -457,9 +459,7 @@ fn test_golden_template() {
|
||||
|
||||
/// Helper: create a post, publish it, then modify a specific field in the
|
||||
/// file on disk. Run the diff and return all detected field names.
|
||||
fn post_diff_for_field(
|
||||
modify_fn: impl FnOnce(&str) -> String,
|
||||
) -> Vec<String> {
|
||||
fn post_diff_for_field(modify_fn: impl FnOnce(&str) -> String) -> Vec<String> {
|
||||
let (db, dir) = setup();
|
||||
|
||||
let created = post::create_post(
|
||||
@@ -519,9 +519,8 @@ fn test_diff_detects_post_slug() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_post_status() {
|
||||
let fields = post_diff_for_field(|content| {
|
||||
content.replace("status: published", "status: draft")
|
||||
});
|
||||
let fields =
|
||||
post_diff_for_field(|content| content.replace("status: published", "status: draft"));
|
||||
assert!(
|
||||
fields.contains(&"status".to_string()),
|
||||
"expected 'status' in diff fields, got: {fields:?}"
|
||||
@@ -530,9 +529,7 @@ fn test_diff_detects_post_status() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_post_tags() {
|
||||
let fields = post_diff_for_field(|content| {
|
||||
content.replace(" - tag1", " - changed-tag")
|
||||
});
|
||||
let fields = post_diff_for_field(|content| content.replace(" - tag1", " - changed-tag"));
|
||||
assert!(
|
||||
fields.contains(&"tags".to_string()),
|
||||
"expected 'tags' in diff fields, got: {fields:?}"
|
||||
@@ -541,9 +538,7 @@ fn test_diff_detects_post_tags() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_post_categories() {
|
||||
let fields = post_diff_for_field(|content| {
|
||||
content.replace(" - cat1", " - changed-cat")
|
||||
});
|
||||
let fields = post_diff_for_field(|content| content.replace(" - cat1", " - changed-cat"));
|
||||
assert!(
|
||||
fields.contains(&"categories".to_string()),
|
||||
"expected 'categories' in diff fields, got: {fields:?}"
|
||||
@@ -578,9 +573,7 @@ fn test_diff_detects_post_author() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_post_language() {
|
||||
let fields = post_diff_for_field(|content| {
|
||||
content.replace("language: en", "language: de")
|
||||
});
|
||||
let fields = post_diff_for_field(|content| content.replace("language: en", "language: de"));
|
||||
assert!(
|
||||
fields.contains(&"language".to_string()),
|
||||
"expected 'language' in diff fields, got: {fields:?}"
|
||||
@@ -687,9 +680,7 @@ fn test_diff_detects_post_published_at() {
|
||||
|
||||
/// Helper: import media, then modify a field in the sidecar file.
|
||||
/// Returns the list of diff field names detected.
|
||||
fn media_diff_for_field(
|
||||
modify_fn: impl FnOnce(&str) -> String,
|
||||
) -> Vec<String> {
|
||||
fn media_diff_for_field(modify_fn: impl FnOnce(&str) -> String) -> Vec<String> {
|
||||
let (db, dir) = setup();
|
||||
|
||||
let source_path = create_test_image(dir.path());
|
||||
@@ -764,10 +755,7 @@ fn test_diff_detects_media_caption() {
|
||||
#[test]
|
||||
fn test_diff_detects_media_author() {
|
||||
let fields = media_diff_for_field(|content| {
|
||||
content.replace(
|
||||
"author: \"Original Author\"",
|
||||
"author: \"Changed Author\"",
|
||||
)
|
||||
content.replace("author: \"Original Author\"", "author: \"Changed Author\"")
|
||||
});
|
||||
assert!(
|
||||
fields.contains(&"author".to_string()),
|
||||
@@ -778,10 +766,7 @@ fn test_diff_detects_media_author() {
|
||||
#[test]
|
||||
fn test_diff_detects_media_tags() {
|
||||
let fields = media_diff_for_field(|content| {
|
||||
content.replace(
|
||||
"tags: [\"original-tag\"]",
|
||||
"tags: [\"changed-tag\"]",
|
||||
)
|
||||
content.replace("tags: [\"original-tag\"]", "tags: [\"changed-tag\"]")
|
||||
});
|
||||
assert!(
|
||||
fields.contains(&"tags".to_string()),
|
||||
@@ -791,9 +776,7 @@ fn test_diff_detects_media_tags() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_media_language() {
|
||||
let fields = media_diff_for_field(|content| {
|
||||
content.replace("language: en", "language: de")
|
||||
});
|
||||
let fields = media_diff_for_field(|content| content.replace("language: en", "language: de"));
|
||||
assert!(
|
||||
fields.contains(&"language".to_string()),
|
||||
"expected 'language' in media diff fields, got: {fields:?}"
|
||||
@@ -804,9 +787,7 @@ fn test_diff_detects_media_language() {
|
||||
|
||||
/// Helper: insert a template in DB + write file, then modify a field in
|
||||
/// the file. Returns the list of diff field names.
|
||||
fn template_diff_for_field(
|
||||
modify_fn: impl FnOnce(&str) -> String,
|
||||
) -> Vec<String> {
|
||||
fn template_diff_for_field(modify_fn: impl FnOnce(&str) -> String) -> Vec<String> {
|
||||
let (db, dir) = setup();
|
||||
|
||||
let tpl = Template {
|
||||
@@ -870,9 +851,8 @@ fn test_diff_detects_template_title() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_template_kind() {
|
||||
let fields = template_diff_for_field(|content| {
|
||||
content.replace("kind: \"post\"", "kind: \"list\"")
|
||||
});
|
||||
let fields =
|
||||
template_diff_for_field(|content| content.replace("kind: \"post\"", "kind: \"list\""));
|
||||
assert!(
|
||||
fields.contains(&"kind".to_string()),
|
||||
"expected 'kind' in template diff fields, got: {fields:?}"
|
||||
@@ -881,9 +861,8 @@ fn test_diff_detects_template_kind() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_template_enabled() {
|
||||
let fields = template_diff_for_field(|content| {
|
||||
content.replace("enabled: true", "enabled: false")
|
||||
});
|
||||
let fields =
|
||||
template_diff_for_field(|content| content.replace("enabled: true", "enabled: false"));
|
||||
assert!(
|
||||
fields.contains(&"enabled".to_string()),
|
||||
"expected 'enabled' in template diff fields, got: {fields:?}"
|
||||
@@ -892,9 +871,7 @@ fn test_diff_detects_template_enabled() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_template_version() {
|
||||
let fields = template_diff_for_field(|content| {
|
||||
content.replace("version: 1", "version: 99")
|
||||
});
|
||||
let fields = template_diff_for_field(|content| content.replace("version: 1", "version: 99"));
|
||||
assert!(
|
||||
fields.contains(&"version".to_string()),
|
||||
"expected 'version' in template diff fields, got: {fields:?}"
|
||||
@@ -905,9 +882,7 @@ fn test_diff_detects_template_version() {
|
||||
|
||||
/// Helper: insert a script in DB + write file, then modify a field in the
|
||||
/// file. Returns the list of diff field names.
|
||||
fn script_diff_for_field(
|
||||
modify_fn: impl FnOnce(&str) -> String,
|
||||
) -> Vec<String> {
|
||||
fn script_diff_for_field(modify_fn: impl FnOnce(&str) -> String) -> Vec<String> {
|
||||
let (db, dir) = setup();
|
||||
|
||||
let script = Script {
|
||||
@@ -995,9 +970,8 @@ fn test_diff_detects_script_entrypoint() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_script_enabled() {
|
||||
let fields = script_diff_for_field(|content| {
|
||||
content.replace("enabled: true", "enabled: false")
|
||||
});
|
||||
let fields =
|
||||
script_diff_for_field(|content| content.replace("enabled: true", "enabled: false"));
|
||||
assert!(
|
||||
fields.contains(&"enabled".to_string()),
|
||||
"expected 'enabled' in script diff fields, got: {fields:?}"
|
||||
@@ -1006,9 +980,7 @@ fn test_diff_detects_script_enabled() {
|
||||
|
||||
#[test]
|
||||
fn test_diff_detects_script_version() {
|
||||
let fields = script_diff_for_field(|content| {
|
||||
content.replace("version: 1", "version: 42")
|
||||
});
|
||||
let fields = script_diff_for_field(|content| content.replace("version: 1", "version: 42"));
|
||||
assert!(
|
||||
fields.contains(&"version".to_string()),
|
||||
"expected 'version' in script diff fields, got: {fields:?}"
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bds_core::db::Database;
|
||||
use bds_core::db::queries::project::insert_project;
|
||||
use bds_core::db::queries::template::insert_template;
|
||||
use bds_core::db::Database;
|
||||
use bds_core::engine::generation::{
|
||||
PublishedPostSource, apply_validation_sections, generate_starter_site,
|
||||
sections_from_validation_report,
|
||||
load_published_post_source, sections_from_validation_report,
|
||||
};
|
||||
use bds_core::engine::meta::write_category_meta_json;
|
||||
use bds_core::engine::validate_site::validate_site;
|
||||
use bds_core::model::{CategorySettings, Post, PostStatus, Project, ProjectMetadata, Template, TemplateKind, TemplateStatus};
|
||||
use bds_core::model::{
|
||||
CategorySettings, Post, PostStatus, Project, ProjectMetadata, Template, TemplateKind,
|
||||
TemplateStatus,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn make_project() -> Project {
|
||||
@@ -95,6 +98,24 @@ fn setup() -> (Database, TempDir) {
|
||||
(db, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reopened_draft_generation_uses_last_published_file() {
|
||||
let (_db, dir) = setup();
|
||||
let mut post = make_post("reopened", 1_710_000_000_000);
|
||||
post.status = PostStatus::Draft;
|
||||
post.content = Some("Unpublished draft body".into());
|
||||
post.file_path = "posts/2024/03/reopened.md".into();
|
||||
let frontmatter = bds_core::util::frontmatter::PostFrontmatter::from_post(&post).to_yaml();
|
||||
let file = bds_core::util::frontmatter::format_frontmatter(&frontmatter, "Published body");
|
||||
std::fs::create_dir_all(dir.path().join("posts/2024/03")).unwrap();
|
||||
std::fs::write(dir.path().join(&post.file_path), file).unwrap();
|
||||
|
||||
let source = load_published_post_source(dir.path(), post)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(source.body_markdown, "Published body");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generation_engine_writes_core_and_single_post_artifacts() {
|
||||
let (db, dir) = setup();
|
||||
@@ -110,7 +131,8 @@ fn generation_engine_writes_core_and_single_post_artifacts() {
|
||||
},
|
||||
];
|
||||
|
||||
let report = generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
let report =
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
|
||||
assert!(report.written_paths.contains(&"index.html".to_string()));
|
||||
assert!(report.written_paths.contains(&"calendar.json".to_string()));
|
||||
@@ -118,10 +140,26 @@ fn generation_engine_writes_core_and_single_post_artifacts() {
|
||||
assert!(report.written_paths.contains(&"feed.xml".to_string()));
|
||||
assert!(report.written_paths.contains(&"atom.xml".to_string()));
|
||||
assert!(report.written_paths.contains(&"sitemap.xml".to_string()));
|
||||
assert!(report.written_paths.contains(&"assets/pico.min.css".to_string()));
|
||||
assert!(report.written_paths.contains(&"assets/tag-cloud.js".to_string()));
|
||||
assert!(report.written_paths.contains(&"2024/03/09/hello/index.html".to_string()));
|
||||
assert!(report.written_paths.contains(&"2024/03/10/next/index.html".to_string()));
|
||||
assert!(
|
||||
report
|
||||
.written_paths
|
||||
.contains(&"assets/pico.min.css".to_string())
|
||||
);
|
||||
assert!(
|
||||
report
|
||||
.written_paths
|
||||
.contains(&"assets/tag-cloud.js".to_string())
|
||||
);
|
||||
assert!(
|
||||
report
|
||||
.written_paths
|
||||
.contains(&"2024/03/09/hello/index.html".to_string())
|
||||
);
|
||||
assert!(
|
||||
report
|
||||
.written_paths
|
||||
.contains(&"2024/03/10/next/index.html".to_string())
|
||||
);
|
||||
|
||||
assert!(dir.path().join("index.html").exists());
|
||||
assert!(dir.path().join("rss.xml").exists());
|
||||
@@ -152,13 +190,17 @@ fn multilingual_generation_writes_language_aware_atom_and_sitemap_routes() {
|
||||
body_markdown: "Hallo Welt".into(),
|
||||
}];
|
||||
|
||||
let report = generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "de").unwrap();
|
||||
let report =
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "de").unwrap();
|
||||
|
||||
assert!(report.written_paths.contains(&"en/atom.xml".to_string()));
|
||||
assert!(report.written_paths.contains(&"en/sitemap.xml".to_string()));
|
||||
|
||||
let atom = std::fs::read_to_string(dir.path().join("en/atom.xml")).unwrap();
|
||||
assert!(atom.contains("<link href=\"https://example.com/en/\" rel=\"alternate\" />") || atom.contains("<link href=\"https://example.com/en\" rel=\"alternate\" />"));
|
||||
assert!(
|
||||
atom.contains("<link href=\"https://example.com/en/\" rel=\"alternate\" />")
|
||||
|| atom.contains("<link href=\"https://example.com/en\" rel=\"alternate\" />")
|
||||
);
|
||||
assert!(atom.contains("<link href=\"https://example.com/en/atom.xml\" rel=\"self\" />"));
|
||||
|
||||
let sitemap = std::fs::read_to_string(dir.path().join("sitemap.xml")).unwrap();
|
||||
@@ -223,7 +265,8 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
||||
},
|
||||
];
|
||||
|
||||
let report = generate_starter_site(db.conn(), dir.path(), "p1", &make_metadata(), &posts, "en").unwrap();
|
||||
let report =
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &make_metadata(), &posts, "en").unwrap();
|
||||
|
||||
for asset in [
|
||||
"images/close.png",
|
||||
@@ -232,17 +275,29 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
||||
"images/prev.png",
|
||||
] {
|
||||
assert!(report.written_paths.contains(&asset.to_string()));
|
||||
assert!(dir.path().join(asset).exists(), "missing bundled image {asset}");
|
||||
assert!(
|
||||
dir.path().join(asset).exists(),
|
||||
"missing bundled image {asset}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(!report.written_paths.contains(&"category/hidden/index.html".to_string()));
|
||||
assert!(report.written_paths.contains(&"category/featured/index.html".to_string()));
|
||||
assert!(
|
||||
!report
|
||||
.written_paths
|
||||
.contains(&"category/hidden/index.html".to_string())
|
||||
);
|
||||
assert!(
|
||||
report
|
||||
.written_paths
|
||||
.contains(&"category/featured/index.html".to_string())
|
||||
);
|
||||
|
||||
let index_html = std::fs::read_to_string(dir.path().join("index.html")).unwrap();
|
||||
assert!(!index_html.contains("Hidden Post"));
|
||||
assert!(index_html.contains("[Featured Post|false]"));
|
||||
|
||||
let featured_html = std::fs::read_to_string(dir.path().join("category/featured/index.html")).unwrap();
|
||||
let featured_html =
|
||||
std::fs::read_to_string(dir.path().join("category/featured/index.html")).unwrap();
|
||||
assert!(featured_html.contains("FEATURED:[Featured Post|false]"));
|
||||
|
||||
let feed = std::fs::read_to_string(dir.path().join("feed.xml")).unwrap();
|
||||
@@ -263,15 +318,21 @@ fn generation_engine_skips_unchanged_outputs_on_second_run() {
|
||||
body_markdown: "Hello **world**".into(),
|
||||
}];
|
||||
|
||||
let first = generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
let second = generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
let first =
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
let second =
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||
|
||||
assert!(!first.written_paths.is_empty());
|
||||
assert!(second.skipped_paths.contains(&"index.html".to_string()));
|
||||
assert!(second.skipped_paths.contains(&"calendar.json".to_string()));
|
||||
assert!(second.skipped_paths.contains(&"rss.xml".to_string()));
|
||||
assert!(second.skipped_paths.contains(&"feed.xml".to_string()));
|
||||
assert!(second.skipped_paths.contains(&"assets/pico.min.css".to_string()));
|
||||
assert!(
|
||||
second
|
||||
.skipped_paths
|
||||
.contains(&"assets/pico.min.css".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -313,7 +374,9 @@ fn apply_validation_repairs_core_section_outputs() {
|
||||
|
||||
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
let sections = sections_from_validation_report(&report);
|
||||
let apply_report = apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions).unwrap();
|
||||
let apply_report =
|
||||
apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions)
|
||||
.unwrap();
|
||||
let repaired = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
|
||||
assert!(!apply_report.written_paths.is_empty() || !apply_report.skipped_paths.is_empty());
|
||||
@@ -339,12 +402,22 @@ fn apply_validation_removes_extra_section_outputs() {
|
||||
std::fs::write(extra_dir.join("index.html"), "ghost").unwrap();
|
||||
|
||||
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
assert!(report.extra_pages.contains(&"tag/ghost/index.html".to_string()));
|
||||
assert!(
|
||||
report
|
||||
.extra_pages
|
||||
.contains(&"tag/ghost/index.html".to_string())
|
||||
);
|
||||
let sections = sections_from_validation_report(&report);
|
||||
let apply_report = apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions).unwrap();
|
||||
let apply_report =
|
||||
apply_validation_sections(db.conn(), dir.path(), "p1", &metadata, &posts, §ions)
|
||||
.unwrap();
|
||||
let repaired = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
|
||||
assert!(apply_report.deleted_paths.contains(&"tag/ghost/index.html".to_string()));
|
||||
assert!(
|
||||
apply_report
|
||||
.deleted_paths
|
||||
.contains(&"tag/ghost/index.html".to_string())
|
||||
);
|
||||
assert!(repaired.extra_pages.is_empty());
|
||||
}
|
||||
|
||||
@@ -368,4 +441,4 @@ fn site_validation_uses_html_output_directory_when_present() {
|
||||
assert!(report.missing_pages.is_empty());
|
||||
assert!(report.extra_pages.is_empty());
|
||||
assert!(report.stale_pages.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::fs;
|
||||
|
||||
use bds_core::db::Database;
|
||||
use bds_core::db::queries::generated_file_hash::get_generated_file_hash;
|
||||
use bds_core::db::queries::project::insert_project;
|
||||
use bds_core::db::Database;
|
||||
use bds_core::model::{Post, PostStatus, Project};
|
||||
use bds_core::render::{
|
||||
GeneratedWriteOutcome, build_calendar_json, build_core_generation_paths,
|
||||
write_generated_file,
|
||||
GeneratedWriteOutcome, build_calendar_json, build_core_generation_paths, write_generated_file,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -72,7 +71,11 @@ fn generated_write_skips_unchanged_content() {
|
||||
|
||||
let stored = get_generated_file_hash(db.conn(), "p1", "index.html").unwrap();
|
||||
assert_eq!(stored.relative_path, "index.html");
|
||||
assert!(fs::read_to_string(dir.path().join("index.html")).unwrap().contains("changed"));
|
||||
assert!(
|
||||
fs::read_to_string(dir.path().join("index.html"))
|
||||
.unwrap()
|
||||
.contains("changed")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -85,7 +88,10 @@ fn generated_write_rewrites_missing_file_even_when_hash_matches() {
|
||||
|
||||
assert_eq!(first, GeneratedWriteOutcome::Written);
|
||||
assert_eq!(second, GeneratedWriteOutcome::Written);
|
||||
assert_eq!(fs::read_to_string(dir.path().join("index.html")).unwrap(), "hello");
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join("index.html")).unwrap(),
|
||||
"hello"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -98,7 +104,10 @@ fn generated_write_rewrites_stale_file_even_when_db_hash_matches() {
|
||||
|
||||
assert_eq!(first, GeneratedWriteOutcome::Written);
|
||||
assert_eq!(second, GeneratedWriteOutcome::Written);
|
||||
assert_eq!(fs::read_to_string(dir.path().join("index.html")).unwrap(), "hello");
|
||||
assert_eq!(
|
||||
fs::read_to_string(dir.path().join("index.html")).unwrap(),
|
||||
"hello"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -131,4 +140,4 @@ fn calendar_json_groups_posts_by_year_month_day() {
|
||||
assert_eq!(parsed["months"]["2024-04"], 1);
|
||||
assert_eq!(parsed["days"]["2024-03-09"], 2);
|
||||
assert_eq!(parsed["days"]["2024-04-09"], 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ fn markdown_filter_expands_builtin_macros_from_runtime_context() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_filter_marks_unsupported_macros_visibly() {
|
||||
fn markdown_filter_leaves_unknown_macros_verbatim() {
|
||||
let template = "{{ body | markdown: nil, nil, canonical_post_path_by_slug, canonical_media_path_by_source_path, language, language_prefix }}";
|
||||
let partials = HashMap::new();
|
||||
let context = serde_json::json!({
|
||||
@@ -104,9 +104,8 @@ fn markdown_filter_marks_unsupported_macros_visibly() {
|
||||
});
|
||||
|
||||
let rendered = render_liquid_template(template, &partials, &context).unwrap();
|
||||
assert!(rendered.contains("macro-unsupported"));
|
||||
assert!(rendered.contains("Unsupported macro"));
|
||||
assert!(rendered.contains("custom_block"));
|
||||
assert!(rendered.contains("[[custom_block foo=bar]]"));
|
||||
assert!(!rendered.contains("macro-unsupported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -123,11 +122,13 @@ fn starter_single_post_template_renders_with_partials() {
|
||||
),
|
||||
(
|
||||
"partials/language-switcher".to_string(),
|
||||
include_str!("../../../assets/starter-templates/partials/language-switcher.liquid").to_string(),
|
||||
include_str!("../../../assets/starter-templates/partials/language-switcher.liquid")
|
||||
.to_string(),
|
||||
),
|
||||
(
|
||||
"partials/menu-items".to_string(),
|
||||
"{% for item in items %}<a href=\"{{ item.href }}\">{{ item.title }}</a>{% endfor %}".to_string(),
|
||||
"{% for item in items %}<a href=\"{{ item.href }}\">{{ item.title }}</a>{% endfor %}"
|
||||
.to_string(),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -167,4 +168,4 @@ fn starter_single_post_template_renders_with_partials() {
|
||||
assert!(rendered.contains("<strong>world</strong>"));
|
||||
assert!(rendered.contains("href=\"/2024/03/09/hello\""));
|
||||
assert!(rendered.contains("data-pagefind-body"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::collections::HashMap;
|
||||
|
||||
use bds_core::model::{Post, PostStatus, Tag, Template, TemplateKind, TemplateStatus};
|
||||
use bds_core::render::{
|
||||
RenderCategorySettings, RenderTemplateLookup, TemplateLookupError,
|
||||
render_markdown_to_html, resolve_post_template,
|
||||
RenderCategorySettings, RenderTemplateLookup, TemplateLookupError, render_markdown_to_html,
|
||||
resolve_post_template,
|
||||
};
|
||||
|
||||
fn make_post() -> Post {
|
||||
@@ -70,7 +70,11 @@ fn template_lookup_prefers_post_specific_template() {
|
||||
post.tags = vec!["rust".into()];
|
||||
post.categories = vec!["article".into()];
|
||||
|
||||
let templates = vec![make_template("post"), make_template("tag-template"), make_template("custom")];
|
||||
let templates = vec![
|
||||
make_template("post"),
|
||||
make_template("tag-template"),
|
||||
make_template("custom"),
|
||||
];
|
||||
let tags = vec![make_tag("rust", Some("tag-template"))];
|
||||
let mut categories = HashMap::new();
|
||||
categories.insert(
|
||||
@@ -163,7 +167,10 @@ fn template_lookup_errors_when_explicit_template_missing() {
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err, TemplateLookupError::MissingExplicitTemplate("missing".into()));
|
||||
assert_eq!(
|
||||
err,
|
||||
TemplateLookupError::MissingExplicitTemplate("missing".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -171,4 +178,4 @@ fn markdown_render_produces_html() {
|
||||
let html = render_markdown_to_html("# Hello\n\nA paragraph with **bold** text.");
|
||||
assert!(html.contains("<h1>Hello</h1>"));
|
||||
assert!(html.contains("<strong>bold</strong>"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,18 @@ fn post_status_serde_matches_db_values() {
|
||||
// spec: status: draft | published | archived
|
||||
use bds_core::model::PostStatus;
|
||||
|
||||
assert_eq!(serde_json::to_string(&PostStatus::Draft).unwrap(), "\"draft\"");
|
||||
assert_eq!(serde_json::to_string(&PostStatus::Published).unwrap(), "\"published\"");
|
||||
assert_eq!(serde_json::to_string(&PostStatus::Archived).unwrap(), "\"archived\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PostStatus::Draft).unwrap(),
|
||||
"\"draft\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PostStatus::Published).unwrap(),
|
||||
"\"published\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&PostStatus::Archived).unwrap(),
|
||||
"\"archived\""
|
||||
);
|
||||
|
||||
// round-trip
|
||||
let d: PostStatus = serde_json::from_str("\"draft\"").unwrap();
|
||||
@@ -44,10 +53,22 @@ fn post_status_serde_matches_db_values() {
|
||||
fn template_kind_serde_matches_db_values() {
|
||||
use bds_core::model::TemplateKind;
|
||||
|
||||
assert_eq!(serde_json::to_string(&TemplateKind::Post).unwrap(), "\"post\"");
|
||||
assert_eq!(serde_json::to_string(&TemplateKind::List).unwrap(), "\"list\"");
|
||||
assert_eq!(serde_json::to_string(&TemplateKind::NotFound).unwrap(), "\"not_found\"");
|
||||
assert_eq!(serde_json::to_string(&TemplateKind::Partial).unwrap(), "\"partial\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&TemplateKind::Post).unwrap(),
|
||||
"\"post\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&TemplateKind::List).unwrap(),
|
||||
"\"list\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&TemplateKind::NotFound).unwrap(),
|
||||
"\"not_found\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&TemplateKind::Partial).unwrap(),
|
||||
"\"partial\""
|
||||
);
|
||||
|
||||
let nf: TemplateKind = serde_json::from_str("\"not_found\"").unwrap();
|
||||
assert_eq!(nf, TemplateKind::NotFound);
|
||||
@@ -59,8 +80,14 @@ fn template_kind_serde_matches_db_values() {
|
||||
fn template_status_serde_matches_db_values() {
|
||||
use bds_core::model::TemplateStatus;
|
||||
|
||||
assert_eq!(serde_json::to_string(&TemplateStatus::Draft).unwrap(), "\"draft\"");
|
||||
assert_eq!(serde_json::to_string(&TemplateStatus::Published).unwrap(), "\"published\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&TemplateStatus::Draft).unwrap(),
|
||||
"\"draft\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&TemplateStatus::Published).unwrap(),
|
||||
"\"published\""
|
||||
);
|
||||
}
|
||||
|
||||
// ── spec: schema.allium — Script kind: macro | utility | transform ──
|
||||
@@ -69,9 +96,18 @@ fn template_status_serde_matches_db_values() {
|
||||
fn script_kind_serde_matches_db_values() {
|
||||
use bds_core::model::ScriptKind;
|
||||
|
||||
assert_eq!(serde_json::to_string(&ScriptKind::Macro).unwrap(), "\"macro\"");
|
||||
assert_eq!(serde_json::to_string(&ScriptKind::Utility).unwrap(), "\"utility\"");
|
||||
assert_eq!(serde_json::to_string(&ScriptKind::Transform).unwrap(), "\"transform\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&ScriptKind::Macro).unwrap(),
|
||||
"\"macro\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&ScriptKind::Utility).unwrap(),
|
||||
"\"utility\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&ScriptKind::Transform).unwrap(),
|
||||
"\"transform\""
|
||||
);
|
||||
|
||||
let t: ScriptKind = serde_json::from_str("\"transform\"").unwrap();
|
||||
assert_eq!(t, ScriptKind::Transform);
|
||||
@@ -83,8 +119,14 @@ fn script_kind_serde_matches_db_values() {
|
||||
fn script_status_serde_matches_db_values() {
|
||||
use bds_core::model::ScriptStatus;
|
||||
|
||||
assert_eq!(serde_json::to_string(&ScriptStatus::Draft).unwrap(), "\"draft\"");
|
||||
assert_eq!(serde_json::to_string(&ScriptStatus::Published).unwrap(), "\"published\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&ScriptStatus::Draft).unwrap(),
|
||||
"\"draft\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&ScriptStatus::Published).unwrap(),
|
||||
"\"published\""
|
||||
);
|
||||
}
|
||||
|
||||
// ── spec: post.allium — content_location invariant ──
|
||||
@@ -105,7 +147,10 @@ fn content_location_published_posts_null_content_in_fixture() {
|
||||
|
||||
assert!(!rows.is_empty());
|
||||
for (slug, content) in &rows {
|
||||
assert!(content.is_none(), "spec: published post '{slug}' must have NULL content in DB");
|
||||
assert!(
|
||||
content.is_none(),
|
||||
"spec: published post '{slug}' must have NULL content in DB"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +168,10 @@ fn content_location_draft_posts_have_content_in_fixture() {
|
||||
|
||||
assert!(!rows.is_empty());
|
||||
for (slug, content) in &rows {
|
||||
assert!(content.is_some(), "spec: draft post '{slug}' must have content in DB");
|
||||
assert!(
|
||||
content.is_some(),
|
||||
"spec: draft post '{slug}' must have content in DB"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +186,8 @@ fn post_status_transitions_all_valid() {
|
||||
"INSERT INTO projects (id, name, slug, created_at, updated_at, is_active) \
|
||||
VALUES ('p1', 'test', 'test', 1000, 1000, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, file_path, created_at, updated_at) \
|
||||
VALUES ('post1', 'p1', 'Test', 'test', 'draft', '', 1000, 1000)",
|
||||
@@ -157,11 +206,15 @@ fn post_status_transitions_all_valid() {
|
||||
|
||||
for (from, to) in transitions {
|
||||
// Set to 'from' state first
|
||||
conn.execute("UPDATE posts SET status = ?1 WHERE id = 'post1'", [from]).unwrap();
|
||||
conn.execute("UPDATE posts SET status = ?1 WHERE id = 'post1'", [from])
|
||||
.unwrap();
|
||||
// Transition to 'to' state
|
||||
conn.execute("UPDATE posts SET status = ?1 WHERE id = 'post1'", [to]).unwrap();
|
||||
conn.execute("UPDATE posts SET status = ?1 WHERE id = 'post1'", [to])
|
||||
.unwrap();
|
||||
let status: String = conn
|
||||
.query_row("SELECT status FROM posts WHERE id = 'post1'", [], |r| r.get(0))
|
||||
.query_row("SELECT status FROM posts WHERE id = 'post1'", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(status, to, "transition {from} -> {to} failed");
|
||||
}
|
||||
@@ -177,7 +230,8 @@ fn slug_frozen_after_publish_semantics() {
|
||||
"INSERT INTO projects (id, name, slug, created_at, updated_at, is_active) \
|
||||
VALUES ('p1', 'test', 'test', 1000, 1000, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, status, file_path, created_at, updated_at, published_at) \
|
||||
VALUES ('post1', 'p1', 'Test', 'test', 'published', 'posts/2024/01/test.md', 1000, 1000, 1000)",
|
||||
@@ -186,9 +240,16 @@ fn slug_frozen_after_publish_semantics() {
|
||||
|
||||
// published_at is set — slug should be considered frozen
|
||||
let published_at: Option<i64> = conn
|
||||
.query_row("SELECT published_at FROM posts WHERE id = 'post1'", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT published_at FROM posts WHERE id = 'post1'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(published_at.is_some(), "spec: is_slug_frozen = published_at != null");
|
||||
assert!(
|
||||
published_at.is_some(),
|
||||
"spec: is_slug_frozen = published_at != null"
|
||||
);
|
||||
|
||||
// A never-published draft has no published_at — slug is mutable
|
||||
conn.execute(
|
||||
@@ -197,9 +258,16 @@ fn slug_frozen_after_publish_semantics() {
|
||||
[],
|
||||
).unwrap();
|
||||
let draft_published_at: Option<i64> = conn
|
||||
.query_row("SELECT published_at FROM posts WHERE id = 'post2'", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT published_at FROM posts WHERE id = 'post2'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(draft_published_at.is_none(), "spec: unpublished draft has no published_at");
|
||||
assert!(
|
||||
draft_published_at.is_none(),
|
||||
"spec: unpublished draft has no published_at"
|
||||
);
|
||||
}
|
||||
|
||||
// ── spec: project.allium — SingleActiveProject invariant ──
|
||||
@@ -209,7 +277,11 @@ fn slug_frozen_after_publish_semantics() {
|
||||
fn single_active_project_in_fixture() {
|
||||
let conn = fixture_db();
|
||||
let active_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM projects WHERE is_active = 1", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM projects WHERE is_active = 1",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(active_count, 1, "spec: exactly one project is active");
|
||||
}
|
||||
@@ -247,7 +319,10 @@ fn unique_translation_per_post_language_in_fixture() {
|
||||
.unwrap()
|
||||
.map(|r| r.unwrap())
|
||||
.collect();
|
||||
assert!(dupes.is_empty(), "spec: translation unique per (post, language)");
|
||||
assert!(
|
||||
dupes.is_empty(),
|
||||
"spec: translation unique per (post, language)"
|
||||
);
|
||||
}
|
||||
|
||||
// ── spec: schema.allium — Post defaults ──
|
||||
@@ -260,13 +335,15 @@ fn post_defaults_match_spec() {
|
||||
"INSERT INTO projects (id, name, slug, created_at, updated_at, is_active) \
|
||||
VALUES ('p1', 'test', 'test', 1000, 1000, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
// Insert with minimal columns to test defaults
|
||||
conn.execute(
|
||||
"INSERT INTO posts (id, project_id, title, slug, created_at, updated_at) \
|
||||
VALUES ('min1', 'p1', 'Minimal', 'minimal', 1000, 1000)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (status, do_not_translate, file_path): (String, bool, String) = conn
|
||||
.query_row(
|
||||
@@ -291,12 +368,14 @@ fn script_defaults_match_spec() {
|
||||
"INSERT INTO projects (id, name, slug, created_at, updated_at, is_active) \
|
||||
VALUES ('p1', 'test', 'test', 1000, 1000, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO scripts (id, project_id, slug, title, file_path, created_at, updated_at) \
|
||||
VALUES ('s1', 'p1', 'test', 'Test', 'scripts/test.lua', 1000, 1000)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (kind, entrypoint, enabled, version, status): (String, String, bool, i32, String) = conn
|
||||
.query_row(
|
||||
@@ -322,12 +401,14 @@ fn template_defaults_match_spec() {
|
||||
"INSERT INTO projects (id, name, slug, created_at, updated_at, is_active) \
|
||||
VALUES ('p1', 'test', 'test', 1000, 1000, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO templates (id, project_id, slug, title, file_path, created_at, updated_at) \
|
||||
VALUES ('t1', 'p1', 'test', 'Test', 'templates/test.liquid', 1000, 1000)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (kind, enabled, version, status): (String, bool, i32, String) = conn
|
||||
.query_row(
|
||||
@@ -351,12 +432,14 @@ fn tag_unique_name_per_project_enforced() {
|
||||
"INSERT INTO projects (id, name, slug, created_at, updated_at, is_active) \
|
||||
VALUES ('p1', 'test', 'test', 1000, 1000, 1)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO tags (id, project_id, name, created_at, updated_at) \
|
||||
VALUES ('t1', 'p1', 'rust', 1000, 1000)",
|
||||
[],
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Same name, same project — must fail
|
||||
let result = conn.execute(
|
||||
@@ -364,7 +447,10 @@ fn tag_unique_name_per_project_enforced() {
|
||||
VALUES ('t2', 'p1', 'rust', 1000, 1000)",
|
||||
[],
|
||||
);
|
||||
assert!(result.is_err(), "spec: duplicate tag name in same project must fail");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"spec: duplicate tag name in same project must fail"
|
||||
);
|
||||
}
|
||||
|
||||
// ── spec: post.allium — Slug generation algorithm ──
|
||||
@@ -447,16 +533,37 @@ fn published_post_file_paths_follow_date_layout() {
|
||||
|
||||
#[test]
|
||||
fn notification_entity_serde_matches_db_values() {
|
||||
use bds_core::model::{NotificationEntity, NotificationAction};
|
||||
use bds_core::model::{NotificationAction, NotificationEntity};
|
||||
|
||||
assert_eq!(serde_json::to_string(&NotificationEntity::Post).unwrap(), "\"post\"");
|
||||
assert_eq!(serde_json::to_string(&NotificationEntity::Media).unwrap(), "\"media\"");
|
||||
assert_eq!(serde_json::to_string(&NotificationEntity::Script).unwrap(), "\"script\"");
|
||||
assert_eq!(serde_json::to_string(&NotificationEntity::Template).unwrap(), "\"template\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationEntity::Post).unwrap(),
|
||||
"\"post\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationEntity::Media).unwrap(),
|
||||
"\"media\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationEntity::Script).unwrap(),
|
||||
"\"script\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationEntity::Template).unwrap(),
|
||||
"\"template\""
|
||||
);
|
||||
|
||||
assert_eq!(serde_json::to_string(&NotificationAction::Created).unwrap(), "\"created\"");
|
||||
assert_eq!(serde_json::to_string(&NotificationAction::Updated).unwrap(), "\"updated\"");
|
||||
assert_eq!(serde_json::to_string(&NotificationAction::Deleted).unwrap(), "\"deleted\"");
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationAction::Created).unwrap(),
|
||||
"\"created\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationAction::Updated).unwrap(),
|
||||
"\"updated\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&NotificationAction::Deleted).unwrap(),
|
||||
"\"deleted\""
|
||||
);
|
||||
}
|
||||
|
||||
// ── spec: generation.allium / publishing.allium — SshMode values ──
|
||||
@@ -493,7 +600,11 @@ fn fts5_tables_exist_in_fixture() {
|
||||
[],
|
||||
|r| r.get::<_, i64>(0),
|
||||
);
|
||||
assert_eq!(result.unwrap(), 1, "spec: posts_fts virtual table must exist");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
1,
|
||||
"spec: posts_fts virtual table must exist"
|
||||
);
|
||||
|
||||
// media_fts
|
||||
let result = conn.query_row(
|
||||
@@ -501,5 +612,9 @@ fn fts5_tables_exist_in_fixture() {
|
||||
[],
|
||||
|r| r.get::<_, i64>(0),
|
||||
);
|
||||
assert_eq!(result.unwrap(), 1, "spec: media_fts virtual table must exist");
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
1,
|
||||
"spec: media_fts virtual table must exist"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user