772 lines
30 KiB
Rust
772 lines
30 KiB
Rust
use rusqlite::Connection;
|
|
|
|
mod embedded {
|
|
use refinery::embed_migrations;
|
|
embed_migrations!("migrations");
|
|
}
|
|
|
|
/// Run all embedded migrations against the given connection using refinery.
|
|
///
|
|
/// Creates the full bDS schema as specified in specs/schema.allium.
|
|
/// Uses refinery for proper versioned migration tracking.
|
|
pub fn run_migrations(conn: &mut Connection) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
embedded::migrations::runner().run(conn)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn setup() -> Connection {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
|
|
run_migrations(&mut conn).unwrap();
|
|
conn
|
|
}
|
|
|
|
/// Helper: insert a project row and return its id.
|
|
fn insert_project(conn: &Connection, id: &str, slug: &str) {
|
|
conn.execute(
|
|
"INSERT INTO projects (id, name, slug, is_active, created_at, updated_at)
|
|
VALUES (?1, 'Test', ?2, 0, 1000, 1000)",
|
|
rusqlite::params![id, slug],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Helper: insert a post row and return its id.
|
|
fn insert_post(conn: &Connection, id: &str, project_id: &str, slug: &str) {
|
|
conn.execute(
|
|
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
|
VALUES (?1, ?2, 'Test Post', ?3, 'draft', 1000, 1000)",
|
|
rusqlite::params![id, project_id, slug],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Helper: insert a media row.
|
|
fn insert_media(conn: &Connection, id: &str, project_id: &str) {
|
|
conn.execute(
|
|
"INSERT INTO media (id, project_id, filename, original_name, mime_type, size, file_path, sidecar_path, created_at, updated_at)
|
|
VALUES (?1, ?2, 'img.jpg', 'photo.jpg', 'image/jpeg', 12345, '/media/img.jpg', '/media/img.jpg.meta', 1000, 1000)",
|
|
rusqlite::params![id, project_id],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
// ================================================================
|
|
// TABLE EXISTENCE — all tables from schema.allium must exist
|
|
// ================================================================
|
|
|
|
#[test]
|
|
fn all_tables_exist() {
|
|
let conn = setup();
|
|
let expected = [
|
|
"projects", "posts", "post_translations", "media", "media_translations",
|
|
"tags", "templates", "scripts", "post_links", "post_media", "settings",
|
|
"generated_file_hashes", "chat_conversations", "chat_messages", "ai_providers",
|
|
"ai_models", "ai_model_modalities", "ai_catalog_meta", "embedding_keys",
|
|
"dismissed_duplicate_pairs", "import_definitions", "db_notifications",
|
|
];
|
|
for table in &expected {
|
|
let count: i64 = conn
|
|
.query_row(
|
|
&format!("SELECT COUNT(*) FROM {table}"),
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap_or_else(|e| panic!("table '{table}' should be queryable: {e}"));
|
|
assert_eq!(count, 0, "table '{table}' should start empty");
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// REFINERY TRACKING — verify migration history table exists
|
|
// ================================================================
|
|
|
|
#[test]
|
|
fn refinery_schema_history_exists() {
|
|
let conn = setup();
|
|
let count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM refinery_schema_history",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert!(count >= 1, "refinery should track at least one migration");
|
|
}
|
|
|
|
// ================================================================
|
|
// UNIQUE INDEX ENFORCEMENT — spec invariant tests
|
|
// ================================================================
|
|
|
|
#[test]
|
|
fn unique_project_slug() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
let err = conn.execute(
|
|
"INSERT INTO projects (id, name, slug, is_active, created_at, updated_at)
|
|
VALUES ('p2', 'Other', 'blog', 0, 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate project slug must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_post_slug_per_project() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
let err = conn.execute(
|
|
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
|
VALUES ('post2', 'p1', 'Other', 'hello', 'draft', 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate post slug within same project must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn same_post_slug_different_project_ok() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog1");
|
|
insert_project(&conn, "p2", "blog2");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
conn.execute(
|
|
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
|
|
VALUES ('post2', 'p2', 'Other', 'hello', 'draft', 1000, 1000)",
|
|
[],
|
|
)
|
|
.expect("same slug in different project should be allowed");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_translation_per_post_language() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
conn.execute(
|
|
"INSERT INTO post_translations (id, project_id, translation_for, language, title, status, created_at, updated_at)
|
|
VALUES ('t1', 'p1', 'post1', 'de', 'Hallo', 'draft', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO post_translations (id, project_id, translation_for, language, title, status, created_at, updated_at)
|
|
VALUES ('t2', 'p1', 'post1', 'de', 'Hallo2', 'draft', 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate (translation_for, language) must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_media_translation_per_media_language() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_media(&conn, "m1", "p1");
|
|
conn.execute(
|
|
"INSERT INTO media_translations (id, project_id, translation_for, language, created_at, updated_at)
|
|
VALUES ('mt1', 'p1', 'm1', 'de', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO media_translations (id, project_id, translation_for, language, created_at, updated_at)
|
|
VALUES ('mt2', 'p1', 'm1', 'de', 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate (media translation_for, language) must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_tag_name_per_project() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO tags (id, project_id, name, created_at, updated_at)
|
|
VALUES ('t1', 'p1', 'rust', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO tags (id, project_id, name, created_at, updated_at)
|
|
VALUES ('t2', 'p1', 'rust', 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate tag name within same project must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_template_slug_per_project() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO templates (id, project_id, slug, title, kind, file_path, created_at, updated_at)
|
|
VALUES ('tpl1', 'p1', 'default', 'Default', 'post', 'templates/default.liquid', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO templates (id, project_id, slug, title, kind, file_path, created_at, updated_at)
|
|
VALUES ('tpl2', 'p1', 'default', 'Default2', 'list', 'templates/default.liquid', 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate template slug within same project must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_script_slug_per_project() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO scripts (id, project_id, slug, title, kind, file_path, created_at, updated_at)
|
|
VALUES ('s1', 'p1', 'gallery', 'Gallery', 'macro', 'scripts/gallery.lua', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO scripts (id, project_id, slug, title, kind, file_path, created_at, updated_at)
|
|
VALUES ('s2', 'p1', 'gallery', 'Gallery2', 'utility', 'scripts/gallery.lua', 1000, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate script slug within same project must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_post_media_link() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
insert_media(&conn, "m1", "p1");
|
|
conn.execute(
|
|
"INSERT INTO post_media (id, project_id, post_id, media_id, sort_order, created_at)
|
|
VALUES ('pm1', 'p1', 'post1', 'm1', 0, 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO post_media (id, project_id, post_id, media_id, sort_order, created_at)
|
|
VALUES ('pm2', 'p1', 'post1', 'm1', 1, 1000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate (post_id, media_id) must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_generated_file_hash() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO generated_file_hashes (project_id, relative_path, content_hash, updated_at)
|
|
VALUES ('p1', 'index.html', 'abc123', 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO generated_file_hashes (project_id, relative_path, content_hash, updated_at)
|
|
VALUES ('p1', 'index.html', 'def456', 2000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate (project_id, relative_path) must be rejected");
|
|
}
|
|
|
|
#[test]
|
|
fn unique_dismissed_duplicate_pair() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO dismissed_duplicate_pairs (id, project_id, post_id_a, post_id_b, dismissed_at)
|
|
VALUES ('d1', 'p1', 'a', 'b', 1000)",
|
|
[],
|
|
).unwrap();
|
|
let err = conn.execute(
|
|
"INSERT INTO dismissed_duplicate_pairs (id, project_id, post_id_a, post_id_b, dismissed_at)
|
|
VALUES ('d2', 'p1', 'a', 'b', 2000)",
|
|
[],
|
|
);
|
|
assert!(err.is_err(), "duplicate (project_id, post_id_a, post_id_b) must be rejected");
|
|
}
|
|
|
|
// ================================================================
|
|
// READ/WRITE ROUND-TRIP — core entity tables
|
|
// ================================================================
|
|
|
|
#[test]
|
|
fn roundtrip_project() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO projects (id, name, slug, description, data_path, is_active, created_at, updated_at)
|
|
VALUES ('p1', 'My Blog', 'my-blog', 'A blog', '/data', 1, 1700000000, 1700000001)",
|
|
[],
|
|
).unwrap();
|
|
let (id, name, slug, desc, dp, active, ca, ua): (String, String, String, Option<String>, Option<String>, i64, i64, i64) = conn
|
|
.query_row("SELECT id, name, slug, description, data_path, is_active, created_at, updated_at FROM projects WHERE id = 'p1'", [], |row| {
|
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?))
|
|
}).unwrap();
|
|
assert_eq!(id, "p1");
|
|
assert_eq!(name, "My Blog");
|
|
assert_eq!(slug, "my-blog");
|
|
assert_eq!(desc.as_deref(), Some("A blog"));
|
|
assert_eq!(dp.as_deref(), Some("/data"));
|
|
assert_eq!(active, 1);
|
|
assert_eq!(ca, 1700000000);
|
|
assert_eq!(ua, 1700000001);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_post_with_all_fields() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO posts (id, project_id, title, slug, excerpt, content, status, author,
|
|
created_at, updated_at, published_at, file_path, checksum, tags, categories,
|
|
template_slug, language, do_not_translate,
|
|
published_title, published_content, published_tags, published_categories, published_excerpt)
|
|
VALUES ('post1', 'p1', 'Hello', 'hello', 'Summary', 'Body text', 'draft', 'Alice',
|
|
1700000000, 1700000001, NULL, '', 'abc123',
|
|
'[\"rust\",\"blog\"]', '[\"tech\"]', 'custom-tpl', 'en', 0,
|
|
NULL, NULL, NULL, NULL, NULL)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let title: String = conn.query_row(
|
|
"SELECT title FROM posts WHERE id = 'post1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(title, "Hello");
|
|
|
|
let tags: String = conn.query_row(
|
|
"SELECT tags FROM posts WHERE id = 'post1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(tags, "[\"rust\",\"blog\"]");
|
|
|
|
let content: Option<String> = conn.query_row(
|
|
"SELECT content FROM posts WHERE id = 'post1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(content.as_deref(), Some("Body text"));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_published_post_null_content() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO posts (id, project_id, title, slug, content, status, file_path, created_at, updated_at, published_at)
|
|
VALUES ('post1', 'p1', 'Published', 'pub', NULL, 'published', 'posts/2024/01/pub.md', 1700000000, 1700000001, 1700000000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let content: Option<String> = conn.query_row(
|
|
"SELECT content FROM posts WHERE id = 'post1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert!(content.is_none(), "published post content must be null in DB");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_post_translation() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
conn.execute(
|
|
"INSERT INTO post_translations (id, project_id, translation_for, language, title, excerpt, content, status, created_at, updated_at, file_path, checksum)
|
|
VALUES ('t1', 'p1', 'post1', 'de', 'Hallo', 'Zusammenfassung', 'Inhalt', 'draft', 1000, 1000, '', NULL)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (lang, title): (String, String) = conn.query_row(
|
|
"SELECT language, title FROM post_translations WHERE id = 't1'", [], |r| Ok((r.get(0)?, r.get(1)?))
|
|
).unwrap();
|
|
assert_eq!(lang, "de");
|
|
assert_eq!(title, "Hallo");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_media() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO media (id, project_id, filename, original_name, mime_type, size, width, height,
|
|
title, alt, caption, author, file_path, sidecar_path, created_at, updated_at, checksum, tags, language)
|
|
VALUES ('m1', 'p1', 'abc.jpg', 'photo.jpg', 'image/jpeg', 50000, 1920, 1080,
|
|
'Sunset', 'A sunset', 'Beautiful sunset', 'Bob', '/media/abc.jpg', '/media/abc.jpg.meta',
|
|
1000, 1000, 'hash123', '[\"nature\"]', 'en')",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (orig, w, h, tags): (String, Option<i32>, Option<i32>, String) = conn.query_row(
|
|
"SELECT original_name, width, height, tags FROM media WHERE id = 'm1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))
|
|
).unwrap();
|
|
assert_eq!(orig, "photo.jpg");
|
|
assert_eq!(w, Some(1920));
|
|
assert_eq!(h, Some(1080));
|
|
assert_eq!(tags, "[\"nature\"]");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_media_translation() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_media(&conn, "m1", "p1");
|
|
conn.execute(
|
|
"INSERT INTO media_translations (id, project_id, translation_for, language, title, alt, caption, created_at, updated_at)
|
|
VALUES ('mt1', 'p1', 'm1', 'de', 'Sonnenuntergang', 'Ein Sonnenuntergang', 'Schön', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (title, alt): (Option<String>, Option<String>) = conn.query_row(
|
|
"SELECT title, alt FROM media_translations WHERE id = 'mt1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?))
|
|
).unwrap();
|
|
assert_eq!(title.as_deref(), Some("Sonnenuntergang"));
|
|
assert_eq!(alt.as_deref(), Some("Ein Sonnenuntergang"));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_tag() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO tags (id, project_id, name, color, post_template_slug, created_at, updated_at)
|
|
VALUES ('t1', 'p1', 'rust', '#ff5733', 'tag-tpl', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (name, color, tpl): (String, Option<String>, Option<String>) = conn.query_row(
|
|
"SELECT name, color, post_template_slug FROM tags WHERE id = 't1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))
|
|
).unwrap();
|
|
assert_eq!(name, "rust");
|
|
assert_eq!(color.as_deref(), Some("#ff5733"));
|
|
assert_eq!(tpl.as_deref(), Some("tag-tpl"));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_template() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO templates (id, project_id, slug, title, kind, enabled, version, file_path, status, content, created_at, updated_at)
|
|
VALUES ('tpl1', 'p1', 'default', 'Default', 'post', 1, 3, 'templates/default.liquid', 'published', NULL, 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (kind, ver, status, content): (String, i32, String, Option<String>) = conn.query_row(
|
|
"SELECT kind, version, status, content FROM templates WHERE id = 'tpl1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))
|
|
).unwrap();
|
|
assert_eq!(kind, "post");
|
|
assert_eq!(ver, 3);
|
|
assert_eq!(status, "published");
|
|
assert!(content.is_none(), "published template content should be null");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_script() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO scripts (id, project_id, slug, title, kind, entrypoint, enabled, version, file_path, status, content, created_at, updated_at)
|
|
VALUES ('s1', 'p1', 'gallery', 'Gallery', 'macro', 'render', 1, 1, 'scripts/gallery.lua', 'draft', 'return html', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (kind, ep, content): (String, String, Option<String>) = conn.query_row(
|
|
"SELECT kind, entrypoint, content FROM scripts WHERE id = 's1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))
|
|
).unwrap();
|
|
assert_eq!(kind, "macro");
|
|
assert_eq!(ep, "render");
|
|
assert_eq!(content.as_deref(), Some("return html"));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_post_link() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
insert_post(&conn, "post2", "p1", "world");
|
|
conn.execute(
|
|
"INSERT INTO post_links (id, source_post_id, target_post_id, link_text, created_at)
|
|
VALUES ('pl1', 'post1', 'post2', 'see also', 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (src, tgt, txt): (String, String, Option<String>) = conn.query_row(
|
|
"SELECT source_post_id, target_post_id, link_text FROM post_links WHERE id = 'pl1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))
|
|
).unwrap();
|
|
assert_eq!(src, "post1");
|
|
assert_eq!(tgt, "post2");
|
|
assert_eq!(txt.as_deref(), Some("see also"));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_post_media_link() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
insert_post(&conn, "post1", "p1", "hello");
|
|
insert_media(&conn, "m1", "p1");
|
|
conn.execute(
|
|
"INSERT INTO post_media (id, project_id, post_id, media_id, sort_order, created_at)
|
|
VALUES ('pm1', 'p1', 'post1', 'm1', 5, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let order: i32 = conn.query_row(
|
|
"SELECT sort_order FROM post_media WHERE id = 'pm1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(order, 5);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_settings() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO settings (key, value, updated_at) VALUES ('theme', 'dark', 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let val: String = conn.query_row(
|
|
"SELECT value FROM settings WHERE key = 'theme'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(val, "dark");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_generated_file_hash() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO generated_file_hashes (project_id, relative_path, content_hash, updated_at)
|
|
VALUES ('p1', 'index.html', 'sha256abc', 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let hash: String = conn.query_row(
|
|
"SELECT content_hash FROM generated_file_hashes WHERE project_id = 'p1' AND relative_path = 'index.html'",
|
|
[], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(hash, "sha256abc");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_chat_conversation() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO chat_conversations (id, title, model, created_at, updated_at)
|
|
VALUES ('c1', 'Test Chat', 'gpt-4', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let title: String = conn.query_row(
|
|
"SELECT title FROM chat_conversations WHERE id = 'c1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(title, "Test Chat");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_chat_message() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO chat_conversations (id, title, created_at, updated_at)
|
|
VALUES ('c1', 'Chat', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
conn.execute(
|
|
"INSERT INTO chat_messages (conversation_id, role, content, created_at)
|
|
VALUES ('c1', 'user', 'Hello', 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (role, content): (String, Option<String>) = conn.query_row(
|
|
"SELECT role, content FROM chat_messages WHERE conversation_id = 'c1'", [],
|
|
|r| Ok((r.get(0)?, r.get(1)?))
|
|
).unwrap();
|
|
assert_eq!(role, "user");
|
|
assert_eq!(content.as_deref(), Some("Hello"));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_ai_provider_and_model() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO ai_providers (id, name, updated_at) VALUES ('openai', 'OpenAI', 1000)",
|
|
[],
|
|
).unwrap();
|
|
conn.execute(
|
|
"INSERT INTO ai_models (provider, model_id, name, context_window, max_input_tokens, max_output_tokens, updated_at)
|
|
VALUES ('openai', 'gpt-4', 'GPT-4', 128000, 128000, 4096, 1000)",
|
|
[],
|
|
).unwrap();
|
|
conn.execute(
|
|
"INSERT INTO ai_model_modalities (provider, model_id, direction, modality)
|
|
VALUES ('openai', 'gpt-4', 'input', 'text')",
|
|
[],
|
|
).unwrap();
|
|
|
|
let name: String = conn.query_row(
|
|
"SELECT name FROM ai_models WHERE provider = 'openai' AND model_id = 'gpt-4'",
|
|
[], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(name, "GPT-4");
|
|
|
|
let modality: String = conn.query_row(
|
|
"SELECT modality FROM ai_model_modalities WHERE provider = 'openai' AND model_id = 'gpt-4'",
|
|
[], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(modality, "text");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_ai_catalog_meta() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO ai_catalog_meta (key, value) VALUES ('etag', 'abc')",
|
|
[],
|
|
).unwrap();
|
|
|
|
let val: String = conn.query_row(
|
|
"SELECT value FROM ai_catalog_meta WHERE key = 'etag'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(val, "abc");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_embedding_keys() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO embedding_keys (label, post_id, project_id, content_hash, vector)
|
|
VALUES (1, 'post1', 'p1', 'hash1', 'base64vector')",
|
|
[],
|
|
).unwrap();
|
|
|
|
let vec: String = conn.query_row(
|
|
"SELECT vector FROM embedding_keys WHERE label = 1", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(vec, "base64vector");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_dismissed_duplicate_pair() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO dismissed_duplicate_pairs (id, project_id, post_id_a, post_id_b, dismissed_at)
|
|
VALUES ('d1', 'p1', 'a', 'b', 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let count: i64 = conn.query_row(
|
|
"SELECT COUNT(*) FROM dismissed_duplicate_pairs WHERE project_id = 'p1'",
|
|
[], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_import_definition() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO import_definitions (id, project_id, name, wxr_file_path, created_at, updated_at)
|
|
VALUES ('i1', 'p1', 'WP Import', '/exports/wp.xml', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let name: String = conn.query_row(
|
|
"SELECT name FROM import_definitions WHERE id = 'i1'", [], |r| r.get(0)
|
|
).unwrap();
|
|
assert_eq!(name, "WP Import");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_db_notification() {
|
|
let conn = setup();
|
|
conn.execute(
|
|
"INSERT INTO db_notifications (entity_type, entity_id, action, from_cli, created_at)
|
|
VALUES ('post', 'post1', 'created', 1, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (etype, action, cli): (String, String, i64) = conn.query_row(
|
|
"SELECT entity_type, action, from_cli FROM db_notifications WHERE entity_id = 'post1'",
|
|
[], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?))
|
|
).unwrap();
|
|
assert_eq!(etype, "post");
|
|
assert_eq!(action, "created");
|
|
assert_eq!(cli, 1);
|
|
}
|
|
|
|
// ================================================================
|
|
// MIGRATION IDEMPOTENCY — running migrations twice must not fail
|
|
// ================================================================
|
|
|
|
#[test]
|
|
fn migrations_idempotent() {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
|
|
run_migrations(&mut conn).unwrap();
|
|
run_migrations(&mut conn).expect("running migrations twice must not fail");
|
|
}
|
|
|
|
// ================================================================
|
|
// DEFAULT VALUES — verify DB defaults match spec
|
|
// ================================================================
|
|
|
|
#[test]
|
|
fn post_defaults() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO posts (id, project_id, title, slug, created_at, updated_at)
|
|
VALUES ('post1', 'p1', 'Test', 'test', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (status, file_path, tags, cats, dnt): (String, String, String, String, i64) = conn.query_row(
|
|
"SELECT status, file_path, tags, categories, do_not_translate FROM posts WHERE id = 'post1'",
|
|
[], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))
|
|
).unwrap();
|
|
assert_eq!(status, "draft", "default status must be 'draft'");
|
|
assert_eq!(file_path, "", "default file_path must be empty string");
|
|
assert_eq!(tags, "[]", "default tags must be '[]'");
|
|
assert_eq!(cats, "[]", "default categories must be '[]'");
|
|
assert_eq!(dnt, 0, "default do_not_translate must be 0");
|
|
}
|
|
|
|
#[test]
|
|
fn template_defaults() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
conn.execute(
|
|
"INSERT INTO templates (id, project_id, slug, title, file_path, created_at, updated_at)
|
|
VALUES ('tpl1', 'p1', 'test', 'Test', 'templates/test.liquid', 1000, 1000)",
|
|
[],
|
|
).unwrap();
|
|
|
|
let (kind, enabled, version, status): (String, i64, i64, String) = conn.query_row(
|
|
"SELECT kind, enabled, version, status FROM templates WHERE id = 'tpl1'",
|
|
[], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))
|
|
).unwrap();
|
|
assert_eq!(kind, "post", "default kind must be 'post'");
|
|
assert_eq!(enabled, 1, "default enabled must be 1");
|
|
assert_eq!(version, 1, "default version must be 1");
|
|
assert_eq!(status, "draft", "default status must be 'draft'");
|
|
}
|
|
|
|
#[test]
|
|
fn script_defaults() {
|
|
let conn = setup();
|
|
insert_project(&conn, "p1", "blog");
|
|
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();
|
|
|
|
let (kind, ep, enabled, version, status): (String, String, i64, i64, String) = conn.query_row(
|
|
"SELECT kind, entrypoint, enabled, version, status FROM scripts WHERE id = 's1'",
|
|
[], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))
|
|
).unwrap();
|
|
assert_eq!(kind, "utility", "default kind must be 'utility'");
|
|
assert_eq!(ep, "render", "default entrypoint must be 'render'");
|
|
assert_eq!(enabled, 1, "default enabled must be 1");
|
|
assert_eq!(version, 1, "default version must be 1");
|
|
assert_eq!(status, "draft", "default status must be 'draft'");
|
|
}
|
|
}
|