Files
cc-switch/src-tauri/src/database/tests.rs
T
Jason c3f2f0798d refactor(database): split monolithic database.rs into modular structure
Split the 1788-line database.rs into a well-organized module directory:

- mod.rs (142 lines): Database struct and initialization
- schema.rs (341 lines): Table creation and schema migrations
- backup.rs (324 lines): SQL import/export and snapshot backup
- migration.rs (240 lines): JSON to SQLite data migration
- tests.rs (280 lines): Unit tests
- dao/providers.rs (254 lines): Provider CRUD operations
- dao/mcp.rs (97 lines): MCP server CRUD operations
- dao/prompts.rs (88 lines): Prompt CRUD operations
- dao/skills.rs (124 lines): Skills CRUD operations
- dao/settings.rs (65 lines): Settings key-value storage

Benefits:
- Max file size reduced from 1788 to 341 lines
- Clear separation of concerns (schema, backup, migration, DAOs)
- Easier to navigate and maintain
- All 107 tests passing with zero API changes
2025-11-25 11:56:08 +08:00

281 lines
8.3 KiB
Rust

//! 数据库模块测试
//!
//! 包含 Schema 迁移和基本功能的测试。
use super::*;
use crate::app_config::MultiAppConfig;
use crate::provider::{Provider, ProviderManager};
use indexmap::IndexMap;
use rusqlite::Connection;
use serde_json::json;
use std::collections::HashMap;
const LEGACY_SCHEMA_SQL: &str = r#"
CREATE TABLE providers (
id TEXT NOT NULL,
app_type TEXT NOT NULL,
name TEXT NOT NULL,
settings_config TEXT NOT NULL,
PRIMARY KEY (id, app_type)
);
CREATE TABLE provider_endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL,
app_type TEXT NOT NULL,
url TEXT NOT NULL
);
CREATE TABLE mcp_servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
server_config TEXT NOT NULL
);
CREATE TABLE prompts (
id TEXT NOT NULL,
app_type TEXT NOT NULL,
name TEXT NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (id, app_type)
);
CREATE TABLE skills (
key TEXT PRIMARY KEY,
installed BOOLEAN NOT NULL DEFAULT 0
);
CREATE TABLE skill_repos (
owner TEXT NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (owner, name)
);
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT
);
"#;
#[derive(Debug)]
struct ColumnInfo {
name: String,
r#type: String,
notnull: i64,
default: Option<String>,
}
fn get_column_info(conn: &Connection, table: &str, column: &str) -> ColumnInfo {
let mut stmt = conn
.prepare(&format!("PRAGMA table_info(\"{table}\");"))
.expect("prepare pragma");
let mut rows = stmt.query([]).expect("query pragma");
while let Some(row) = rows.next().expect("read row") {
let name: String = row.get(1).expect("name");
if name.eq_ignore_ascii_case(column) {
return ColumnInfo {
name,
r#type: row.get::<_, String>(2).expect("type"),
notnull: row.get::<_, i64>(3).expect("notnull"),
default: row.get::<_, Option<String>>(4).ok().flatten(),
};
}
}
panic!("column {table}.{column} not found");
}
fn normalize_default(default: &Option<String>) -> Option<String> {
default
.as_ref()
.map(|s| s.trim_matches('\'').trim_matches('"').to_string())
}
#[test]
fn migration_sets_user_version_when_missing() {
let conn = Connection::open_in_memory().expect("open memory db");
Database::create_tables_on_conn(&conn).expect("create tables");
assert_eq!(
Database::get_user_version(&conn).expect("read version before"),
0
);
Database::apply_schema_migrations_on_conn(&conn).expect("apply migration");
assert_eq!(
Database::get_user_version(&conn).expect("read version after"),
SCHEMA_VERSION
);
}
#[test]
fn migration_rejects_future_version() {
let conn = Connection::open_in_memory().expect("open memory db");
Database::create_tables_on_conn(&conn).expect("create tables");
Database::set_user_version(&conn, SCHEMA_VERSION + 1).expect("set future version");
let err = Database::apply_schema_migrations_on_conn(&conn)
.expect_err("should reject higher version");
assert!(
err.to_string().contains("数据库版本过新"),
"unexpected error: {err}"
);
}
#[test]
fn migration_adds_missing_columns_for_providers() {
let conn = Connection::open_in_memory().expect("open memory db");
// 创建旧版 providers 表,缺少新增列
conn.execute_batch(LEGACY_SCHEMA_SQL)
.expect("seed old schema");
Database::apply_schema_migrations_on_conn(&conn).expect("apply migrations");
// 验证关键新增列已补齐
for (table, column) in [
("providers", "meta"),
("providers", "is_current"),
("provider_endpoints", "added_at"),
("mcp_servers", "enabled_gemini"),
("prompts", "updated_at"),
("skills", "installed_at"),
("skill_repos", "enabled"),
] {
assert!(
Database::has_column(&conn, table, column).expect("check column"),
"{table}.{column} should exist after migration"
);
}
// 验证 meta 列约束保持一致
let meta = get_column_info(&conn, "providers", "meta");
assert_eq!(meta.notnull, 1, "meta should be NOT NULL");
assert_eq!(
normalize_default(&meta.default).as_deref(),
Some("{}"),
"meta default should be '{{}}'"
);
assert_eq!(
Database::get_user_version(&conn).expect("version after migration"),
SCHEMA_VERSION
);
}
#[test]
fn migration_aligns_column_defaults_and_types() {
let conn = Connection::open_in_memory().expect("open memory db");
conn.execute_batch(LEGACY_SCHEMA_SQL)
.expect("seed old schema");
Database::apply_schema_migrations_on_conn(&conn).expect("apply migrations");
let is_current = get_column_info(&conn, "providers", "is_current");
assert_eq!(is_current.r#type, "BOOLEAN");
assert_eq!(is_current.notnull, 1);
assert_eq!(
normalize_default(&is_current.default).as_deref(),
Some("0")
);
let tags = get_column_info(&conn, "mcp_servers", "tags");
assert_eq!(tags.r#type, "TEXT");
assert_eq!(tags.notnull, 1);
assert_eq!(normalize_default(&tags.default).as_deref(), Some("[]"));
let enabled = get_column_info(&conn, "prompts", "enabled");
assert_eq!(enabled.r#type, "BOOLEAN");
assert_eq!(enabled.notnull, 1);
assert_eq!(
normalize_default(&enabled.default).as_deref(),
Some("1")
);
let installed_at = get_column_info(&conn, "skills", "installed_at");
assert_eq!(installed_at.r#type, "INTEGER");
assert_eq!(installed_at.notnull, 1);
assert_eq!(
normalize_default(&installed_at.default).as_deref(),
Some("0")
);
let branch = get_column_info(&conn, "skill_repos", "branch");
assert_eq!(branch.r#type, "TEXT");
assert_eq!(normalize_default(&branch.default).as_deref(), Some("main"));
let skill_repo_enabled = get_column_info(&conn, "skill_repos", "enabled");
assert_eq!(skill_repo_enabled.r#type, "BOOLEAN");
assert_eq!(skill_repo_enabled.notnull, 1);
assert_eq!(
normalize_default(&skill_repo_enabled.default).as_deref(),
Some("1")
);
}
#[test]
fn dry_run_does_not_write_to_disk() {
// Create minimal valid config for migration
let mut apps = HashMap::new();
apps.insert("claude".to_string(), ProviderManager::default());
let config = MultiAppConfig {
version: 2,
apps,
mcp: Default::default(),
prompts: Default::default(),
skills: Default::default(),
common_config_snippets: Default::default(),
claude_common_config_snippet: None,
};
// Dry-run should succeed without any file I/O errors
let result = Database::migrate_from_json_dry_run(&config);
assert!(
result.is_ok(),
"Dry-run should succeed with valid config: {result:?}"
);
}
#[test]
fn dry_run_validates_schema_compatibility() {
// Create config with actual provider data
let mut providers = IndexMap::new();
providers.insert(
"test-provider".to_string(),
Provider {
id: "test-provider".to_string(),
name: "Test Provider".to_string(),
settings_config: json!({
"anthropicApiKey": "sk-test-123",
}),
website_url: None,
category: None,
created_at: Some(1234567890),
sort_index: None,
notes: None,
meta: None,
icon: None,
icon_color: None,
},
);
let mut manager = ProviderManager::default();
manager.providers = providers;
manager.current = "test-provider".to_string();
let mut apps = HashMap::new();
apps.insert("claude".to_string(), manager);
let config = MultiAppConfig {
version: 2,
apps,
mcp: Default::default(),
prompts: Default::default(),
skills: Default::default(),
common_config_snippets: Default::default(),
claude_common_config_snippet: None,
};
// Dry-run should validate the full migration path
let result = Database::migrate_from_json_dry_run(&config);
assert!(
result.is_ok(),
"Dry-run should succeed with provider data: {result:?}"
);
}