From 55c07f58521b8643397437240e5565e51f0bc20e Mon Sep 17 00:00:00 2001 From: t8y2 Date: Fri, 25 Sep 2026 02:33:41 +0800 Subject: [PATCH] fix(dev): stabilize macOS signing and isolate test keys --- CONTRIBUTING.md | 17 + CONTRIBUTING.zh-CN.md | 17 + Cargo.lock | 1 + Makefile | 2 +- crates/dbx-core/Cargo.toml | 2 + crates/dbx-core/src/admin/mq/service.rs | 4 +- crates/dbx-core/src/admin/nacos/service.rs | 4 +- crates/dbx-core/src/ai/agent_explain.rs | 2 +- crates/dbx-core/src/ai/agent_loop.rs | 2 +- crates/dbx-core/src/ai/agent_tools.rs | 12 +- crates/dbx-core/src/ai/plugin_tools.rs | 2 +- crates/dbx-core/src/connection/mod.rs | 17 +- .../dbx-core/src/connection/runtime_config.rs | 2 +- crates/dbx-core/src/data/csv_export.rs | 3 +- crates/dbx-core/src/data/database_export.rs | 3 +- .../src/data/mongodb_import_export.rs | 3 +- crates/dbx-core/src/data/sql_file_import.rs | 6 +- crates/dbx-core/src/data/table_export.rs | 5 +- crates/dbx-core/src/data/table_import.rs | 21 +- crates/dbx-core/src/data/transfer.rs | 2 +- crates/dbx-core/src/data/transfer/ddl_plan.rs | 2 +- .../src/data/transfer/rebuild_tests.rs | 4 +- crates/dbx-core/src/data/transfer_rebuild.rs | 2 +- crates/dbx-core/src/persistence/cloud_sync.rs | 141 ++++--- .../src/persistence/config/governance.rs | 4 +- crates/dbx-core/src/persistence/mod.rs | 2 + .../dbx-core/src/persistence/secret_codec.rs | 62 ++- .../src/persistence/state_persistence.rs | 8 +- crates/dbx-core/src/persistence/storage.rs | 362 +++++++++--------- .../dbx-core/src/persistence/test_storage.rs | 42 ++ crates/dbx-core/src/query/mod.rs | 40 +- crates/dbx-core/src/query/mongo_ops.rs | 4 +- crates/dbx-core/src/query/object_cache.rs | 2 +- crates/dbx-core/src/query/plugin_data.rs | 2 +- crates/dbx-core/src/query/plugin_plan.rs | 4 +- crates/dbx-core/src/query/two_phase_commit.rs | 3 +- crates/dbx-core/src/safety/write_unlock.rs | 4 +- crates/dbx-core/src/scheduled_backup/tests.rs | 4 +- crates/dbx-core/src/schema/mod.rs | 25 +- crates/dbx-core/src/schema/plugin_metadata.rs | 3 +- .../src/schema/plugin_metadata_tests.rs | 3 +- .../tests/database_export_partition_ddl.rs | 7 +- .../tests/database_export_prefetch.rs | 3 +- crates/dbx-core/tests/dump_docs_fixture.rs | 3 +- .../live_clickhouse_query_result_export.rs | 3 +- crates/dbx-core/tests/live_doris.rs | 3 +- .../tests/live_manual_snapshot_rotation.rs | 5 +- .../dbx-core/tests/live_manual_transaction.rs | 3 +- .../tests/live_mongodb_agent_tools.rs | 7 +- .../live_mongodb_legacy_agent_commands.rs | 12 +- crates/dbx-core/tests/live_mysql57.rs | 9 +- .../tests/live_mysql_autocommit_snapshot.rs | 9 +- .../tests/live_mysql_database_export.rs | 13 +- .../tests/live_mysql_export_table_order.rs | 3 +- .../tests/live_mysql_object_statistics.rs | 3 +- crates/dbx-core/tests/live_mysql_transfer.rs | 29 +- .../live_oceanbase_sql_file_transaction.rs | 3 +- .../tests/live_postgres_all_schema_export.rs | 4 +- .../tests/live_postgres_docs_annotations.rs | 3 +- .../tests/live_postgres_docs_snapshot.rs | 3 +- .../tests/live_postgres_error_position.rs | 3 +- .../live_postgres_query_result_export.rs | 13 +- .../dbx-core/tests/live_postgres_transfer.rs | 23 +- .../dbx-core/tests/live_redis_batch_expiry.rs | 3 +- .../tests/live_redis_pool_registry.rs | 3 +- .../tests/live_sqlserver_completion.rs | 12 +- .../tests/live_sqlserver_ddl_fk_display.rs | 3 +- .../live_sqlserver_query_result_export.rs | 3 +- .../dbx-core/tests/live_sqlserver_transfer.rs | 15 +- .../dbx-core/tests/mongodb_column_metadata.rs | 3 +- crates/dbx-core/tests/mongodb_dump_restore.rs | 10 +- crates/dbx-core/tests/mq_token.rs | 3 +- crates/dbx-web/Cargo.toml | 1 + crates/dbx-web/src/demo.rs | 8 +- crates/dbx-web/src/main.rs | 3 +- crates/dbx-web/src/routes/ai.rs | 2 +- crates/dbx-web/src/routes/cloud_sync.rs | 5 +- crates/dbx-web/src/routes/connection.rs | 6 +- crates/dbx-web/src/routes/mongo.rs | 4 +- .../src/routes/mongodb_import_export.rs | 9 +- crates/dbx-web/src/routes/mq.rs | 2 +- .../dbx-web/src/routes/object_cache_tests.rs | 9 +- crates/dbx-web/src/routes/query.rs | 3 +- .../dbx-web/src/routes/query_result_export.rs | 3 +- crates/dbx-web/src/routes/sql_file.rs | 2 +- crates/dbx-web/src/routes/table_export.rs | 3 +- crates/dbx-web/src/routes/transfer.rs | 3 +- package.json | 2 +- scripts/dev-tauri.mjs | 73 ++++ scripts/dev-tauri.test.mjs | 119 ++++++ scripts/fixtures/macos-dev-keychain.c | 29 ++ scripts/macos-dev-signing.mjs | 160 ++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/background_backup.rs | 2 +- src-tauri/src/commands/ai.rs | 2 +- src-tauri/src/commands/cloud_sync.rs | 5 +- src-tauri/src/commands/connection.rs | 21 +- src-tauri/src/commands/mcp_bridge.rs | 6 +- src-tauri/src/commands/query.rs | 3 +- 99 files changed, 1022 insertions(+), 533 deletions(-) create mode 100644 crates/dbx-core/src/persistence/test_storage.rs create mode 100644 scripts/dev-tauri.mjs create mode 100644 scripts/dev-tauri.test.mjs create mode 100644 scripts/fixtures/macos-dev-keychain.c create mode 100644 scripts/macos-dev-signing.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9daf46792c..16059daf16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,23 @@ make docs # preview the documentation site make cargo-check-fast # fast Rust checks ``` +### macOS Development Signing + +Use `make dev`, `make dev-fast`, or `pnpm dev:tauri`. These entry points sign each rebuilt debug executable with a stable, local-only development identity before launching it. The first launch may still ask you to select **Always Allow** for DBX's existing Keychain item; subsequent rebuilds keep the same code identity. Direct `pnpm tauri dev` bypasses this setup. + +The first run creates a dedicated signing keychain and a self-signed certificate under `~/Library/Application Support/DBX/development-signing/`. It adds only this keychain to your user search list, without changing the default keychain or system trust settings. The directory is owner-only (`0700`); its files, including the generated password used to unlock this development-only signing keychain, are owner-only (`0600`). No release private key or DBX connection-encryption key is exported or replaced. Keep this local identity across rebuilds; do not commit or share it. An incomplete, corrupt, or expired identity fails explicitly rather than silently rotating or falling back to ad-hoc signing. + +The runner is restricted to `debug/dbx`, preserves Cargo feature and application arguments, and does not run for Linux, Windows, or release packaging. Custom `CARGO_TARGET_*_RUNNER` variables must be unset for these macOS development entry points. + +Core, desktop and Web storage test fixtures use `dbx_core::persistence::test_storage` (the `test-support` dev-dependency feature). They resolve their own data-directory keys before migration preflight, without accessing the user's Keychain or inheriting `DBX_SECRET_KEY` / `DBX_SECRET_KEY_FILE`. Keep the fixture directory and its key together when testing database copies. + +```bash +node --test scripts/dev-tauri.test.mjs +DBX_TEST_MACOS_KEYCHAIN=1 node --test scripts/dev-tauri.test.mjs +``` + +The opt-in macOS integration test creates and removes a temporary signing keychain. It verifies that an ad-hoc rebuild is denied and that two different builds signed with the same identity can read the same test item with system interaction disabled. + ### JDBC Agent Drivers Agent driver projects live under `agents/`. Java/JDBC driver builds and tests require JDK 21; Gradle can auto-download the toolchain when available. diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index ba9924f848..4e85799d2b 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -42,6 +42,23 @@ make docs # 本地预览文档站 make cargo-check-fast # 快速 Rust 检查 ``` +### macOS 开发签名 + +请使用 `make dev`、`make dev-fast` 或 `pnpm dev:tauri`。这些入口会在每次重新编译后、启动前,用固定的本地开发身份签名。首次启动仍可能需要对 DBX 原有钥匙串条目选择一次“始终允许”,之后重编译保持相同代码身份。直接执行 `pnpm tauri dev` 会绕过此流程。 + +首次运行会在 `~/Library/Application Support/DBX/development-signing/` 创建独立签名钥匙串和自签开发证书,仅将该钥匙串加入当前用户的搜索列表,不更改默认钥匙串或系统信任设置。目录权限为 `0700`,文件权限为 `0600`;其中保存的随机密码只用于解锁这个开发签名钥匙串。不使用发布私钥,也不导出或更换已有连接的加密密钥。请保留这套本地身份,不要提交或分享;配置不完整、损坏或证书过期时会明确报错,不会自动换证书或降级为临时签名。 + +签名 runner 仅接受 `debug/dbx`,保留 Cargo feature 和应用参数,不影响 Linux、Windows 或正式打包。使用这些 macOS 开发入口时,需要取消自定义的 `CARGO_TARGET_*_RUNNER` 环境变量。 + +Core、桌面端和 Web 的 Storage 测试夹具使用 `dbx_core::persistence::test_storage`(仅由开发依赖启用 `test-support` feature),在迁移预检查前选择测试目录自己的密钥,不访问用户钥匙串,也不继承 `DBX_SECRET_KEY` / `DBX_SECRET_KEY_FILE`。测试数据库复制时,应将夹具目录及其密钥一起保留。 + +```bash +node --test scripts/dev-tauri.test.mjs +DBX_TEST_MACOS_KEYCHAIN=1 node --test scripts/dev-tauri.test.mjs +``` + +第二条为 macOS 集成验证:创建并清理临时签名钥匙串,禁止系统交互,证明临时签名重编译会被拒绝,而采用同一稳定身份的两个不同构建仍能读取同一测试条目。 + ### JDBC Agent 驱动 Agent 驱动工程在 `agents/` 目录。Java/JDBC 驱动构建和测试需要 JDK 21;环境允许时 Gradle 可以自动下载对应 toolchain。 diff --git a/Cargo.lock b/Cargo.lock index 551be19bad..ca6a176a30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,7 @@ dependencies = [ "crc", "csv", "dbx-ai-provider", + "dbx-core", "dbx-drivers", "dbx-formats", "dbx-platform", diff --git a/Makefile b/Makefile index dd99ddc476..63bd606767 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ dev: node_modules/.modules.yaml check-tauri-dev-port dev-fast: node_modules/.modules.yaml check-tauri-dev-port # os-keyring must stay in sync with src-tauri defaults: without it the keychain # key is unreadable and the secret-store migration wizard reappears on every launch. - RUST_MIN_STACK=16777216 $(PNPM) tauri dev -- --no-default-features --features duckdb-sidecar,dynamodb,sqlite-bundled,os-keyring + RUST_MIN_STACK=16777216 $(PNPM) dev:tauri -- --no-default-features --features duckdb-sidecar,dynamodb,sqlite-bundled,os-keyring dev-web: node_modules/.modules.yaml $(PNPM) dev:web diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index 4245d739ae..a03e377771 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" license = "Apache-2.0" [features] +test-support = [] default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "system-fonts"] duckdb-sidecar = ["dbx-sql/duckdb-sidecar", "dbx-drivers/duckdb-sidecar"] dynamodb = ["dbx-drivers/dynamodb"] @@ -92,6 +93,7 @@ tempfile = "3" windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Security"] } [dev-dependencies] +dbx-core = { path = ".", default-features = false, features = ["test-support"] } zstd = "0.13" dbx-drivers = { path = "../dbx-drivers", default-features = false, features = ["test-support"] } dbx-plugin-runtime = { path = "../dbx-plugin-runtime", features = ["test-support"] } diff --git a/crates/dbx-core/src/admin/mq/service.rs b/crates/dbx-core/src/admin/mq/service.rs index 502ce639ba..2510a3b569 100644 --- a/crates/dbx-core/src/admin/mq/service.rs +++ b/crates/dbx-core/src/admin/mq/service.rs @@ -894,7 +894,6 @@ async fn ensure_connection_writable(state: &AppState, conn_id: &str, operation: mod tests { use super::*; use crate::models::connection::{ConnectionConfig, DatabaseType}; - use crate::storage::Storage; use std::time::{SystemTime, UNIX_EPOCH}; fn mq_connection(read_only: bool) -> ConnectionConfig { @@ -987,7 +986,8 @@ mod tests { SystemTime::now().duration_since(UNIX_EPOCH).expect("system time should be after UNIX epoch").as_nanos(); let dir = std::env::temp_dir().join(format!("dbx-mq-service-test-{stamp}")); std::fs::create_dir_all(&dir).expect("failed to create test directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("failed to open test storage"); + let storage = + crate::persistence::test_storage::open(&dir.join("storage.db")).await.expect("failed to open test storage"); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); state.configs.write().await.insert(config.id.clone(), config); (state, dir) diff --git a/crates/dbx-core/src/admin/nacos/service.rs b/crates/dbx-core/src/admin/nacos/service.rs index a5afdc2092..0646ce9b3a 100644 --- a/crates/dbx-core/src/admin/nacos/service.rs +++ b/crates/dbx-core/src/admin/nacos/service.rs @@ -639,7 +639,7 @@ mod tests { async fn raw_mutation_requires_writable_connection_before_adapter_build() { let dir = std::env::temp_dir().join(format!("dbx-nacos-service-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut cfg = crate::models::connection::ConnectionConfig { docs_notes_path: None, @@ -721,7 +721,7 @@ mod tests { async fn config_rollback_requires_writable_connection_before_adapter_build() { let dir = std::env::temp_dir().join(format!("dbx-nacos-service-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let cfg = crate::models::connection::ConnectionConfig { docs_notes_path: None, diff --git a/crates/dbx-core/src/ai/agent_explain.rs b/crates/dbx-core/src/ai/agent_explain.rs index c91652b44a..5386655a76 100644 --- a/crates/dbx-core/src/ai/agent_explain.rs +++ b/crates/dbx-core/src/ai/agent_explain.rs @@ -276,7 +276,7 @@ mod tests { "query_timeout_secs": 30 })) .unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config.clone()); state diff --git a/crates/dbx-core/src/ai/agent_loop.rs b/crates/dbx-core/src/ai/agent_loop.rs index c9256b82cc..46c0d539fa 100644 --- a/crates/dbx-core/src/ai/agent_loop.rs +++ b/crates/dbx-core/src/ai/agent_loop.rs @@ -1690,7 +1690,7 @@ mod tests { async fn gate_context(session_id: Option<&str>) -> (tempfile::TempDir, AgentLoopContext) { let temp_dir = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); let ctx = AgentLoopContext { state: Arc::new(AppState::new(storage)), connection_id: "db".to_string(), diff --git a/crates/dbx-core/src/ai/agent_tools.rs b/crates/dbx-core/src/ai/agent_tools.rs index 96d0a2d89c..c6a051f86b 100644 --- a/crates/dbx-core/src/ai/agent_tools.rs +++ b/crates/dbx-core/src/ai/agent_tools.rs @@ -1592,8 +1592,6 @@ mod tests { use crate::db::agent_driver::{AgentDriverClient, AgentLaunchSpec}; #[cfg(unix)] use crate::models::connection::{default_redis_key_separator, ConnectionConfig}; - #[cfg(unix)] - use crate::storage::Storage; #[cfg(unix)] async fn spawn_recording_agent(record_path: &std::path::Path) -> (AgentDriverClient, tempfile::NamedTempFile) { @@ -1789,7 +1787,7 @@ for line in sys.stdin: assert!(!confirmed_execute_query.description.contains("confirmed write")); let temp_dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let call = ToolCall { id: "mongo-find-one".to_string(), @@ -1818,7 +1816,7 @@ for line in sys.stdin: #[tokio::test] async fn mongodb_agent_keeps_all_writes_blocked_after_sql_confirmation() { let temp_dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let permissions = AgentSqlPermissions { allow_writes: true, @@ -2139,7 +2137,7 @@ for line in sys.stdin: let temp_dir = tempfile::tempdir().unwrap(); let record_path = temp_dir.path().join("agent-requests.jsonl"); let (client, _script) = spawn_recording_agent(&record_path).await; - let storage = Storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let connection = agent_test_connection("dameng-1", "Dameng", DatabaseType::Dameng, "APPDB"); state.configs.write().await.insert(connection.id.clone(), connection); @@ -2207,7 +2205,7 @@ for line in sys.stdin: let temp_dir = tempfile::tempdir().unwrap(); let record_path = temp_dir.path().join("agent-requests.jsonl"); let (client, _script) = spawn_recording_agent(&record_path).await; - let storage = Storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let connection = agent_test_connection("mysql-1", "MySQL", DatabaseType::Mysql, "rs_main"); state.configs.write().await.insert(connection.id.clone(), connection); @@ -2605,7 +2603,7 @@ for line in sys.stdin: let temp_dir = tempfile::tempdir().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - let storage = crate::storage::Storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_dir.path().join("storage.db")).await.unwrap(); let state = std::sync::Arc::new(crate::connection::AppState::new(storage)); let tool_call = ToolCall { id: "call-gct".to_string(), diff --git a/crates/dbx-core/src/ai/plugin_tools.rs b/crates/dbx-core/src/ai/plugin_tools.rs index 45bdd44d9a..06becda776 100644 --- a/crates/dbx-core/src/ai/plugin_tools.rs +++ b/crates/dbx-core/src/ai/plugin_tools.rs @@ -1117,7 +1117,7 @@ mod tests { #[tokio::test] async fn stale_tool_bindings_cannot_bypass_revoked_ai_access() { let root = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&root.path().join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&root.path().join("dbx.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.storage.set_ai_plugin_tool_plugin_enabled("io.dbx.kafka", true).await.unwrap(); let set = build_tool_set( diff --git a/crates/dbx-core/src/connection/mod.rs b/crates/dbx-core/src/connection/mod.rs index ce027813d2..2912b10fbd 100644 --- a/crates/dbx-core/src/connection/mod.rs +++ b/crates/dbx-core/src/connection/mod.rs @@ -6925,7 +6925,6 @@ mod tests { }; use crate::query; use crate::schema; - use crate::storage::Storage; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -7139,7 +7138,7 @@ mod tests { async fn apply_session_credential_injects_saved_password_only_for_no_save_connections() { let dir = std::env::temp_dir().join(format!("dbx-core-session-cred-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let _ = state.session_credentials.set("", "conn-a", "s3cret"); @@ -7173,7 +7172,7 @@ mod tests { async fn apply_session_credential_reads_owner_scoped_credentials_only() { let dir = std::env::temp_dir().join(format!("dbx-core-session-cred-owner-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let mut config = mysql_config(None); @@ -7205,7 +7204,7 @@ mod tests { async fn pool_credential_owner_mismatch_prevents_cross_session_pool_reuse() { let dir = std::env::temp_dir().join(format!("dbx-core-pool-owner-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let mut config = mysql_config(None); @@ -7820,7 +7819,7 @@ mod tests { async fn test_app_state() -> (AppState, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-core-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); (AppState::new(storage), dir) } @@ -8064,7 +8063,7 @@ mod tests { async fn app_state_uses_explicit_agent_dir() { let dir = std::env::temp_dir().join(format!("dbx-core-agent-dir-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let agent_dir = dir.join("agents"); let state = AppState::new_with_plugin_and_agent_dir_and_app_version( @@ -8329,7 +8328,7 @@ mod tests { async fn jdbc_plugin_env_uses_managed_jre_when_installed() { let dir = std::env::temp_dir().join(format!("dbx-core-jdbc-managed-jre-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_and_agent_dir_and_app_version( storage, dir.join("plugins"), @@ -8349,7 +8348,7 @@ mod tests { async fn jdbc_plugin_env_keeps_wrapper_fallback_when_managed_jre_is_missing() { let dir = std::env::temp_dir().join(format!("dbx-core-jdbc-missing-jre-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_and_agent_dir_and_app_version( storage, dir.join("plugins"), @@ -8367,7 +8366,7 @@ mod tests { async fn jdbc_plugin_env_uses_custom_java_runtime() { let dir = std::env::temp_dir().join(format!("dbx-core-jdbc-custom-jre-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_and_agent_dir_and_app_version( storage, dir.join("plugins"), diff --git a/crates/dbx-core/src/connection/runtime_config.rs b/crates/dbx-core/src/connection/runtime_config.rs index 1d78e37eab..4f4e6cf5e5 100644 --- a/crates/dbx-core/src/connection/runtime_config.rs +++ b/crates/dbx-core/src/connection/runtime_config.rs @@ -131,7 +131,7 @@ mod tests { async fn app_state() -> (AppState, tempfile::TempDir) { let dir = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); (AppState::new(storage), dir) } diff --git a/crates/dbx-core/src/data/csv_export.rs b/crates/dbx-core/src/data/csv_export.rs index 4c623b770a..6c841dc49f 100644 --- a/crates/dbx-core/src/data/csv_export.rs +++ b/crates/dbx-core/src/data/csv_export.rs @@ -125,7 +125,6 @@ mod tests { use super::{export_table_data_csv_core, CsvQuoteMode, TableCsvExportOptions}; use crate::connection::AppState; use crate::models::connection::{ConnectionConfig, DatabaseType}; - use crate::storage::Storage; fn salesforce_config(id: &str) -> ConnectionConfig { let mut config = serde_json::from_value::(serde_json::json!({ @@ -148,7 +147,7 @@ mod tests { #[tokio::test] async fn salesforce_table_csv_export_is_refused_before_a_file_is_created() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let config = salesforce_config("sfdc-csv"); state.configs.write().await.insert(config.id.clone(), config); diff --git a/crates/dbx-core/src/data/database_export.rs b/crates/dbx-core/src/data/database_export.rs index 6d8c0c31f3..0a9374a60d 100644 --- a/crates/dbx-core/src/data/database_export.rs +++ b/crates/dbx-core/src/data/database_export.rs @@ -4077,7 +4077,6 @@ mod tests { use super::{ExportProgress, LenientExportErrors}; use crate::connection::AppState; use crate::models::connection::DatabaseType; - use crate::storage::Storage; use crate::types::SpatialColumn; use crate::types::{ObjectInfo, ObjectSourceKind, TableInfo}; use serde_json::{json, Value}; @@ -6510,7 +6509,7 @@ mod tests { } async fn test_app_state(scratch_dir: &std::path::Path) -> AppState { - let storage = Storage::open(&scratch_dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&scratch_dir.join("storage.db")).await.unwrap(); AppState::new(storage) } diff --git a/crates/dbx-core/src/data/mongodb_import_export.rs b/crates/dbx-core/src/data/mongodb_import_export.rs index bfeabb9105..097527e65a 100644 --- a/crates/dbx-core/src/data/mongodb_import_export.rs +++ b/crates/dbx-core/src/data/mongodb_import_export.rs @@ -4014,7 +4014,6 @@ mod tests { use crate::db::agent_driver::{AgentDriverClient, AgentLaunchSpec}; use crate::models::connection::ConnectionConfig; - use crate::storage::Storage; let directory = tempfile::tempdir().unwrap(); let mut file = tempfile::NamedTempFile::new_in(directory.path()).unwrap(); @@ -4047,7 +4046,7 @@ for line in sys.stdin: .await .unwrap(); client.try_optional_handshake("test").await.unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": "legacy", diff --git a/crates/dbx-core/src/data/sql_file_import.rs b/crates/dbx-core/src/data/sql_file_import.rs index c9fb89a163..8df34c6297 100644 --- a/crates/dbx-core/src/data/sql_file_import.rs +++ b/crates/dbx-core/src/data/sql_file_import.rs @@ -2665,7 +2665,7 @@ mod tests { #[tokio::test] async fn manual_file_missing_session_never_falls_back_to_ordinary_execution() { let directory = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let request = SqlFileRequest { txn_session_id: Some("expired-session".to_string()), @@ -2945,7 +2945,7 @@ mod tests { async fn file_progress_executor_preserves_success_cancellation_and_failure() { let directory = std::env::temp_dir().join(format!("dbx-file-progress-{}", uuid::Uuid::new_v4())); tokio::fs::create_dir_all(&directory).await.unwrap(); - let storage = crate::storage::Storage::open(&directory.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let comments = "-- no database operation\n".repeat(40_000).into_bytes(); for (bytes, cancel, terminal) in [ @@ -3348,7 +3348,7 @@ mod tests { async fn streaming_gaussdb_on_error_stop_overrides_continue_on_error_at_script_position() { let dir = std::env::temp_dir().join(format!("dbx-sql-file-stop-on-error-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let config: crate::models::connection::ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": "gauss-stream", diff --git a/crates/dbx-core/src/data/table_export.rs b/crates/dbx-core/src/data/table_export.rs index fd207e4b6f..3bc661ec44 100644 --- a/crates/dbx-core/src/data/table_export.rs +++ b/crates/dbx-core/src/data/table_export.rs @@ -2300,7 +2300,6 @@ mod tests { use crate::plugins::{ InstalledPlugin, PluginDriverManifest, PluginDriverSession, PluginManifest, PluginRuntimeEnv, }; - use crate::storage::Storage; use crate::xlsx_export::{build_xlsx_workbook, XlsxWorksheetData}; use serde_json::json; use std::io::Read; @@ -2390,7 +2389,7 @@ mod tests { "query_timeout_secs": 30 })) .unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config.clone()); let export_id = format!("export-{}", uuid::Uuid::new_v4()); @@ -2566,7 +2565,7 @@ mod tests { "query_timeout_secs": 30 })) .unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); let export_id = format!("export-{}", uuid::Uuid::new_v4()); diff --git a/crates/dbx-core/src/data/table_import.rs b/crates/dbx-core/src/data/table_import.rs index 3be73f1171..239bf1f58a 100644 --- a/crates/dbx-core/src/data/table_import.rs +++ b/crates/dbx-core/src/data/table_import.rs @@ -7507,7 +7507,6 @@ where mod tests { use super::*; use crate::models::connection::{ConnectionConfig, DatabaseType}; - use crate::storage::Storage; use crate::xlsx_export::{build_xlsx_workbook_multi, XlsxWorksheetData}; use std::io::{Cursor, Write}; @@ -9493,7 +9492,7 @@ mod tests { #[tokio::test] async fn truncate_xlsx_with_malformed_tail_preserves_existing_rows() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "xlsx-truncate-tail"; let pool_key = format!("{connection_id}:session:import"); @@ -9585,7 +9584,7 @@ mod tests { #[tokio::test] async fn cancelling_xlsx_after_validation_prevents_non_transactional_truncate() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "cancel-xlsx-after-validation"; let pool_key = format!("{connection_id}:session:import"); @@ -9673,7 +9672,7 @@ mod tests { #[tokio::test] async fn cancelling_before_first_truncate_batch_preserves_existing_rows() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "cancel-truncate-first-batch"; let pool_key = format!("{connection_id}:session:import"); @@ -11312,7 +11311,7 @@ mod tests { #[tokio::test] async fn sqlserver_staging_cleanup_failure_invalidates_cached_pool() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let pool_key = "sqlserver-cleanup-failure"; let database_path = dir.path().join("target.db"); @@ -11360,7 +11359,7 @@ mod tests { use std::sync::Arc; let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let pool_key = "cancel-sql-sub-batches:session:import"; let database_path = dir.path().join("target.db"); @@ -11427,7 +11426,7 @@ mod tests { #[tokio::test] async fn pending_postgres_copy_is_not_flushed_after_cancellation() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut accumulator = Some(PostgresCopyAccumulator::with_limits("COPY items".to_string(), 1024, 100)); accumulator.as_mut().unwrap().append_row(b"1\n"); @@ -11455,7 +11454,7 @@ mod tests { #[tokio::test] async fn postgres_copy_internal_flush_stops_before_write_when_cancelled() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let plan = CompiledImportPlan { mapped_source_indexes: vec![0], @@ -11579,7 +11578,7 @@ mod tests { impl SqliteAppendTestContext { async fn new(test_name: &str, max_rows: usize) -> Self { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let pool_key = format!("{test_name}:session:import"); let database_path = dir.path().join("target.db"); @@ -11695,7 +11694,7 @@ mod tests { #[tokio::test] async fn delimited_sqlite_append_import_flushes_the_final_window() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "sqlite-delimited-append"; let pool_key = format!("{connection_id}:session:import"); @@ -11929,7 +11928,7 @@ mod tests { #[tokio::test] async fn oracle_jdbc_import_maps_xls_rows_to_insert_all() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "oracle-jdbc-import"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ diff --git a/crates/dbx-core/src/data/transfer.rs b/crates/dbx-core/src/data/transfer.rs index 333b482194..f71f33f400 100644 --- a/crates/dbx-core/src/data/transfer.rs +++ b/crates/dbx-core/src/data/transfer.rs @@ -10871,7 +10871,7 @@ CREATE TABLE "Other"."prefix""Source"."NAME" ("ID" INT);"#; async fn test_app_state() -> (AppState, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-transfer-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); (AppState::new(storage), dir) } diff --git a/crates/dbx-core/src/data/transfer/ddl_plan.rs b/crates/dbx-core/src/data/transfer/ddl_plan.rs index 8ea74ef67c..3c26c07ba5 100644 --- a/crates/dbx-core/src/data/transfer/ddl_plan.rs +++ b/crates/dbx-core/src/data/transfer/ddl_plan.rs @@ -244,7 +244,7 @@ mod tests { async fn state_fixture() -> (Arc, tempfile::TempDir) { let directory = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new_with_plugin_dir(storage, directory.path().join("plugins"))); (state, directory) } diff --git a/crates/dbx-core/src/data/transfer/rebuild_tests.rs b/crates/dbx-core/src/data/transfer/rebuild_tests.rs index 882e5e95e2..3f872f5ca9 100644 --- a/crates/dbx-core/src/data/transfer/rebuild_tests.rs +++ b/crates/dbx-core/src/data/transfer/rebuild_tests.rs @@ -3,7 +3,7 @@ use serde_json::json; async fn sqlite_fixture() -> (tempfile::TempDir, Arc, TransferRequest, String, String) { let directory = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new_with_plugin_dir(storage, directory.path().join("plugins"))); for id in ["source", "target"] { let path = directory.path().join(format!("{id}.db")); @@ -118,7 +118,7 @@ async fn transfer_rebuild_preview_plans_without_executing_ddl() { #[tokio::test] async fn transfer_keyset_pagination_copies_every_row_across_many_batches() { let directory = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new_with_plugin_dir(storage, directory.path().join("plugins"))); for id in ["source", "target"] { let path = directory.path().join(format!("{id}.db")); diff --git a/crates/dbx-core/src/data/transfer_rebuild.rs b/crates/dbx-core/src/data/transfer_rebuild.rs index d477d17c84..aeb3c41011 100644 --- a/crates/dbx-core/src/data/transfer_rebuild.rs +++ b/crates/dbx-core/src/data/transfer_rebuild.rs @@ -1083,7 +1083,7 @@ mod tests { async fn sqlite_dependency_fixture() -> (AppState, crate::db::sqlite::SqliteHandle, tempfile::TempDir) { let directory = tempfile::tempdir().unwrap(); - let storage = crate::storage::Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let pool = crate::db::sqlite::connect_path_create_if_missing(directory.path().join("target.db").to_str().unwrap()) diff --git a/crates/dbx-core/src/persistence/cloud_sync.rs b/crates/dbx-core/src/persistence/cloud_sync.rs index 356c7cfb68..ab2c1e4b76 100644 --- a/crates/dbx-core/src/persistence/cloud_sync.rs +++ b/crates/dbx-core/src/persistence/cloud_sync.rs @@ -2264,7 +2264,6 @@ mod tests { default_redis_key_separator, ConnectionConfig, DatabaseType, SshTunnelConfig, TransportLayerConfig, }; use crate::persistence::secret_codec::{managed_key_path, SecretKeyPolicy}; - use crate::storage::Storage; fn make_test_config(name: &str, is_default: bool) -> AiConfigItem { AiConfigItem { @@ -2683,7 +2682,7 @@ mod tests { #[tokio::test] async fn webdav_upload_terminates_collection_request_paths() { - let storage = Storage::open(&temp_db_path("webdav-collection-paths")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("webdav-collection-paths")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let (endpoint, server) = spawn_webdav_server(vec![201, 201, 200], true).await; let client = WebDavClient::new(WebDavConfig { @@ -2707,7 +2706,8 @@ mod tests { #[tokio::test] async fn webdav_upload_accepts_existing_collections() { - let storage = Storage::open(&temp_db_path("webdav-existing-collections")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("webdav-existing-collections")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let (endpoint, server) = spawn_webdav_server(vec![405, 405, 200], false).await; let client = WebDavClient::new(WebDavConfig { @@ -2731,7 +2731,8 @@ mod tests { #[tokio::test] async fn webdav_upload_preserves_collection_conflicts() { - let storage = Storage::open(&temp_db_path("webdav-collection-conflict")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("webdav-collection-conflict")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let (endpoint, server) = spawn_webdav_server(vec![409], false).await; let client = WebDavClient::new(WebDavConfig { @@ -2749,7 +2750,7 @@ mod tests { #[tokio::test] async fn webdav_upload_skips_collection_requests_for_single_file() { - let storage = Storage::open(&temp_db_path("webdav-single-file")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("webdav-single-file")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let (endpoint, server) = spawn_webdav_server(vec![200], false).await; let client = WebDavClient::new(WebDavConfig { @@ -2916,7 +2917,8 @@ mod tests { assert_eq!(tls["truststore_password"], ""); assert_eq!(tls["keystore_password"], ""); - let storage = Storage::open(&temp_db_path("cassandra-sensitive-payload")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("cassandra-sensitive-payload")).await.unwrap(); let payload = build_sensitive_payload(&storage, &[config], &[]).await.unwrap(); assert!(payload.connection_secrets.iter().any(|secret| { secret.connection_id == "cassandra" @@ -2999,7 +3001,8 @@ mod tests { #[tokio::test] async fn encrypted_snippet_snapshot_hides_and_restores_the_full_snapshot() { - let storage = Storage::open(&temp_db_path("encrypted-snippet-snapshot")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("encrypted-snippet-snapshot")).await.unwrap(); storage.save_connections(&[postgres_connection("pg", "db-secret")]).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, Some("sync-pass")).await.unwrap(); @@ -3017,7 +3020,8 @@ mod tests { #[tokio::test] async fn encrypted_snippet_can_exclude_secrets_and_keep_local_credentials_on_restore() { - let source = Storage::open(&temp_db_path("snippet-without-secrets-source")).await.unwrap(); + let source = + crate::persistence::test_storage::open(&temp_db_path("snippet-without-secrets-source")).await.unwrap(); source.save_connections(&[postgres_connection("pg", "remote-secret")]).await.unwrap(); let snapshot = build_sync_snapshot(&source, "test-version", None, None).await.unwrap(); assert!(snapshot.encrypted_secrets.is_none()); @@ -3025,7 +3029,8 @@ mod tests { let encrypted = encrypt_snippet_snapshot(&snapshot, "snippet-password").unwrap(); let restored = parse_snippet_snapshot(&serde_json::to_string(&encrypted).unwrap(), Some("snippet-password")).unwrap(); - let target = Storage::open(&temp_db_path("snippet-without-secrets-target")).await.unwrap(); + let target = + crate::persistence::test_storage::open(&temp_db_path("snippet-without-secrets-target")).await.unwrap(); target.save_connections(&[postgres_connection("pg", "local-secret")]).await.unwrap(); let summary = apply_sync_snapshot( @@ -3042,7 +3047,8 @@ mod tests { #[tokio::test] async fn skipping_snippet_secret_restore_keeps_local_credentials() { - let source = Storage::open(&temp_db_path("snippet-skip-secrets-source")).await.unwrap(); + let source = + crate::persistence::test_storage::open(&temp_db_path("snippet-skip-secrets-source")).await.unwrap(); source.save_connections(&[postgres_connection("pg", "remote-secret")]).await.unwrap(); let snapshot = build_sync_snapshot(&source, "test-version", None, Some("secrets-password")).await.unwrap(); assert!(snapshot.encrypted_secrets.is_some()); @@ -3050,7 +3056,8 @@ mod tests { let encrypted = encrypt_snippet_snapshot(&snapshot, "snippet-password").unwrap(); let restored = parse_snippet_snapshot(&serde_json::to_string(&encrypted).unwrap(), Some("snippet-password")).unwrap(); - let target = Storage::open(&temp_db_path("snippet-skip-secrets-target")).await.unwrap(); + let target = + crate::persistence::test_storage::open(&temp_db_path("snippet-skip-secrets-target")).await.unwrap(); target.save_connections(&[postgres_connection("pg", "local-secret")]).await.unwrap(); let summary = apply_sync_snapshot( @@ -3067,7 +3074,7 @@ mod tests { #[tokio::test] async fn existing_encrypted_snippet_rejects_wrong_password_without_patch() { - let storage = Storage::open(&temp_db_path("snippet-password-guard")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("snippet-password-guard")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = encrypt_snippet_snapshot(&snapshot, "correct-password").unwrap(); let (base, server) = @@ -3089,7 +3096,7 @@ mod tests { #[tokio::test] async fn existing_encrypted_snippet_accepts_correct_password_before_patch() { - let storage = Storage::open(&temp_db_path("snippet-password-update")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("snippet-password-update")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = encrypt_snippet_snapshot(&snapshot, "correct-password").unwrap(); let (base, server) = spawn_snippet_server(vec![ @@ -3114,7 +3121,8 @@ mod tests { #[tokio::test] async fn legacy_migration_skips_delete_when_remote_content_changes() { - let storage = Storage::open(&temp_db_path("legacy-snippet-change-guard")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("legacy-snippet-change-guard")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let legacy_content = serde_json::to_string(&snapshot).unwrap(); let changed_content = @@ -3153,7 +3161,7 @@ mod tests { #[tokio::test] async fn pending_legacy_cleanup_survives_response_loss_and_retries_after_restart() { let db = temp_db_path("legacy-snippet-cleanup-retry"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let legacy_content = serde_json::to_string(&snapshot).unwrap(); let (base, server) = spawn_snippet_server(vec![ @@ -3178,7 +3186,7 @@ mod tests { assert_eq!(server.await.unwrap(), vec!["GET /gists/legacy-id HTTP/1.1", "POST /gists HTTP/1.1"]); drop(storage); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let settings = snippet_sync_settings(&storage, SnippetProvider::GitHub).await.unwrap(); assert_eq!(settings.snippet_id.as_deref(), Some("new-id")); assert_eq!(settings.legacy_cleanup_required_id.as_deref(), Some("legacy-id")); @@ -3206,7 +3214,8 @@ mod tests { #[tokio::test] async fn legacy_snippet_migration_preserves_remote_snapshot() { - let storage = Storage::open(&temp_db_path("legacy-snippet-migration-guard")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("legacy-snippet-migration-guard")).await.unwrap(); let local_snapshot = build_sync_snapshot(&storage, "local-version", None, None).await.unwrap(); let remote_snapshot = build_sync_snapshot(&storage, "remote-version", None, None).await.unwrap(); let mut legacy = serde_json::to_value(remote_snapshot).unwrap(); @@ -3228,7 +3237,8 @@ mod tests { #[tokio::test] async fn legacy_snippet_migration_refuses_unverifiable_encrypted_secrets() { - let storage = Storage::open(&temp_db_path("legacy-snippet-migration-secrets")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("legacy-snippet-migration-secrets")).await.unwrap(); storage.save_connections(&[postgres_connection("pg", "db-secret")]).await.unwrap(); let remote_snapshot = build_sync_snapshot(&storage, "remote-version", None, Some("remote-pass")).await.unwrap(); let content = serde_json::to_string(&remote_snapshot).unwrap(); @@ -3254,7 +3264,7 @@ mod tests { #[tokio::test] async fn snippet_sync_id_is_persisted_per_provider() { - let storage = Storage::open(&temp_db_path("snippet-sync-id")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("snippet-sync-id")).await.unwrap(); save_snippet_sync_id(&storage, SnippetProvider::GitHub, Some("github-id")).await.unwrap(); save_snippet_sync_id(&storage, SnippetProvider::Gitee, Some("gitee-id")).await.unwrap(); @@ -3309,7 +3319,7 @@ mod tests { #[tokio::test] async fn gitlab_instances_keep_saved_tokens_ids_and_cleanup_separate() { - let storage = Storage::open(&temp_db_path("gitlab-instance-isolation")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-instance-isolation")).await.unwrap(); let first = "https://gitlab.example.com"; let second = "https://gitlab.other.com"; let config = |instance: &str| SnippetSyncConfig { @@ -3349,7 +3359,7 @@ mod tests { #[tokio::test] async fn gitlab_create_and_download_use_private_personal_snippet_and_raw_file() { - let storage = Storage::open(&temp_db_path("gitlab-create-download")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-create-download")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = encrypt_snippet_snapshot(&snapshot, "password").unwrap(); let (base, server) = spawn_gitlab_server(vec![ @@ -3385,7 +3395,7 @@ mod tests { #[tokio::test] async fn gitlab_download_uses_snippet_raw_url_branch() { - let storage = Storage::open(&temp_db_path("gitlab-raw-url-branch")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-raw-url-branch")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = serde_json::to_string(&encrypt_snippet_snapshot(&snapshot, "password").unwrap()).unwrap(); let (base, server) = spawn_gitlab_server_with_status(vec![ @@ -3416,7 +3426,7 @@ mod tests { #[tokio::test] async fn gitlab_download_falls_back_to_master_when_main_raw_file_is_missing() { - let storage = Storage::open(&temp_db_path("gitlab-master-fallback")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-master-fallback")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = serde_json::to_string(&encrypt_snippet_snapshot(&snapshot, "password").unwrap()).unwrap(); let (base, server) = spawn_gitlab_server_with_status(vec![ @@ -3463,7 +3473,7 @@ mod tests { #[tokio::test] async fn gitlab_update_checks_existing_password_before_put() { - let storage = Storage::open(&temp_db_path("gitlab-password-guard")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-password-guard")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = serde_json::to_string(&encrypt_snippet_snapshot(&snapshot, "password").unwrap()).unwrap(); let (base, server) = spawn_gitlab_server(vec![ @@ -3492,7 +3502,7 @@ mod tests { #[tokio::test] async fn gitlab_wrong_password_never_updates_existing_snippet() { - let storage = Storage::open(&temp_db_path("gitlab-wrong-password")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-wrong-password")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let encrypted = serde_json::to_string(&encrypt_snippet_snapshot(&snapshot, "correct").unwrap()).unwrap(); let (base, server) = @@ -3514,7 +3524,7 @@ mod tests { #[tokio::test] async fn gitlab_legacy_migration_creates_before_deleting_and_tracks_instance() { - let storage = Storage::open(&temp_db_path("gitlab-legacy-migration")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("gitlab-legacy-migration")).await.unwrap(); let snapshot = build_sync_snapshot(&storage, "test-version", None, None).await.unwrap(); let legacy_content = serde_json::to_string(&snapshot).unwrap(); let metadata = serde_json::json!({"files": [{"path": "dbx-sync.json"}]}).to_string(); @@ -3604,7 +3614,7 @@ mod tests { #[tokio::test] async fn webdav_sync_secrets_preference_round_trips_and_clears_passphrase() { - let storage = Storage::open(&temp_db_path("sync-secrets-preference")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("sync-secrets-preference")).await.unwrap(); let status = webdav_sync_secrets_status(&storage).await.unwrap(); assert!(!status.enabled); @@ -3627,7 +3637,7 @@ mod tests { #[tokio::test] async fn saved_sync_passphrase_encrypts_snapshot_secrets_without_exposing_connection_passwords() { - let storage = Storage::open(&temp_db_path("saved-sync-snapshot")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("saved-sync-snapshot")).await.unwrap(); storage.save_connections(&[postgres_connection("pg", "db-secret")]).await.unwrap(); let plain_snapshot = @@ -3651,7 +3661,7 @@ mod tests { async fn legacy_sync_payload_preserves_credentials_but_explicit_empty_clears_them() { let directory = tempfile::tempdir().unwrap(); let database_path = directory.path().join("dbx.db"); - let target = Storage::open_unmigrated(&database_path) + let target = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -3685,7 +3695,7 @@ mod tests { .await .unwrap(); drop(target); - let target = Storage::open_unmigrated(&database_path) + let target = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -3709,7 +3719,7 @@ mod tests { assert_eq!(restored_snippet.token.as_deref(), (!restore_secrets).then_some("local-token")); } drop(target); - let target = Storage::open_unmigrated(&database_path) + let target = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -3725,12 +3735,12 @@ mod tests { async fn metadata_only_sync_roundtrip_preserves_public_url_params_and_target_secrets() { let source_directory = tempfile::tempdir().unwrap(); let target_directory = tempfile::tempdir().unwrap(); - let source = Storage::open_unmigrated(&source_directory.path().join("dbx.db")) + let source = crate::persistence::test_storage::open_unmigrated(&source_directory.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); let target_path = target_directory.path().join("dbx.db"); - let target = Storage::open_unmigrated(&target_path) + let target = crate::persistence::test_storage::open_unmigrated(&target_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -3758,7 +3768,7 @@ mod tests { .await .unwrap(); drop(target); - let target = Storage::open_unmigrated(&target_path) + let target = crate::persistence::test_storage::open_unmigrated(&target_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -3782,7 +3792,7 @@ mod tests { assert!(encrypted.starts_with("dbxenc1.")); let locked_directory = tempfile::tempdir().unwrap(); - let locked_target = Storage::open_unmigrated(&locked_directory.path().join("dbx.db")) + let locked_target = crate::persistence::test_storage::open_unmigrated(&locked_directory.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir) @@ -3831,7 +3841,8 @@ mod tests { #[tokio::test] async fn sync_credentials_are_rewrapped_for_the_destination_device() { - let source = Storage::open(&temp_db_path("sync-global-credentials-source")).await.unwrap(); + let source = + crate::persistence::test_storage::open(&temp_db_path("sync-global-credentials-source")).await.unwrap(); let webdav = WebDavConfig { endpoint: "https://dav.example.test/remote.php/dav/files/alice".to_string(), username: Some("alice".to_string()), @@ -3860,7 +3871,7 @@ mod tests { assert!(credentials.iter().any(|credential| credential.secret == "github-token")); let target_path = temp_db_path("sync-global-credentials-target"); - let target = Storage::open(&target_path).await.unwrap(); + let target = crate::persistence::test_storage::open(&target_path).await.unwrap(); apply_sync_snapshot( &target, &snapshot, @@ -3880,7 +3891,7 @@ mod tests { // database does not affect the restored credentials. drop(target); drop(source); - let target = Storage::open(&target_path).await.unwrap(); + let target = crate::persistence::test_storage::open(&target_path).await.unwrap(); let mut reopened_webdav = webdav.clone(); resolve_webdav_password(&target, &mut reopened_webdav).await.unwrap(); assert_eq!(reopened_webdav.password.as_deref(), Some("webdav-secret")); @@ -3910,7 +3921,7 @@ mod tests { #[tokio::test] async fn plugin_connection_secrets_are_scrubbed_from_public_sync_metadata() { - let storage = Storage::open(&temp_db_path("plugin-public-sync")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("plugin-public-sync")).await.unwrap(); let mut config = postgres_connection("plugin", ""); config.db_type = DatabaseType::Plugin; config.plugin_id = Some("example.plugin".to_string()); @@ -3932,7 +3943,8 @@ mod tests { #[tokio::test] async fn plugin_secrets_export_even_when_primary_password_is_not_saved() { - let storage = Storage::open(&temp_db_path("plugin-secret-without-password")).await.unwrap(); + let storage = + crate::persistence::test_storage::open(&temp_db_path("plugin-secret-without-password")).await.unwrap(); let mut config = postgres_connection("plugin-no-password", ""); config.db_type = DatabaseType::Plugin; config.save_password = false; @@ -3965,7 +3977,8 @@ mod tests { #[tokio::test] async fn excluded_plugin_secrets_do_not_clear_destination_credentials() { - let source = Storage::open(&temp_db_path("plugin-secret-excluded-source")).await.unwrap(); + let source = + crate::persistence::test_storage::open(&temp_db_path("plugin-secret-excluded-source")).await.unwrap(); let mut config = postgres_connection("plugin-preserve", ""); config.db_type = DatabaseType::Plugin; config.plugin_id = Some("example.plugin".to_string()); @@ -3987,7 +4000,8 @@ mod tests { .await .unwrap(); - let target = Storage::open(&temp_db_path("plugin-secret-excluded-target")).await.unwrap(); + let target = + crate::persistence::test_storage::open(&temp_db_path("plugin-secret-excluded-target")).await.unwrap(); let mut target_config = config.clone(); target_config.connection_secrets.insert("api_token".to_string(), "local-plugin-secret".to_string()); target.save_connections(std::slice::from_ref(&target_config)).await.unwrap(); @@ -4007,12 +4021,14 @@ mod tests { #[tokio::test] async fn sync_restore_does_not_revive_password_when_connection_disables_saving() { - let source = Storage::open(&temp_db_path("sync-no-save-password-source")).await.unwrap(); + let source = + crate::persistence::test_storage::open(&temp_db_path("sync-no-save-password-source")).await.unwrap(); source.save_connections(&[postgres_connection("pg", "remote-secret")]).await.unwrap(); let mut snapshot = build_sync_snapshot(&source, "test-version", None, Some("sync-pass")).await.unwrap(); snapshot.connections[0].save_password = false; - let target = Storage::open(&temp_db_path("sync-no-save-password-target")).await.unwrap(); + let target = + crate::persistence::test_storage::open(&temp_db_path("sync-no-save-password-target")).await.unwrap(); apply_sync_snapshot( &target, &snapshot, @@ -4027,11 +4043,11 @@ mod tests { #[tokio::test] async fn wrong_sync_passphrase_does_not_modify_the_destination() { - let source = Storage::open(&temp_db_path("sync-wrong-pass-source")).await.unwrap(); + let source = crate::persistence::test_storage::open(&temp_db_path("sync-wrong-pass-source")).await.unwrap(); source.save_connections(&[postgres_connection("remote", "remote-secret")]).await.unwrap(); let snapshot = build_sync_snapshot(&source, "test-version", None, Some("correct-pass")).await.unwrap(); - let target = Storage::open(&temp_db_path("sync-wrong-pass-target")).await.unwrap(); + let target = crate::persistence::test_storage::open(&temp_db_path("sync-wrong-pass-target")).await.unwrap(); target.save_connections(&[postgres_connection("local", "local-secret")]).await.unwrap(); let before = target.load_connections().await.unwrap(); assert!(apply_sync_snapshot( @@ -4048,14 +4064,16 @@ mod tests { #[tokio::test] async fn legacy_plaintext_snapshot_secrets_are_migrated_only_on_explicit_restore() { - let source = Storage::open(&temp_db_path("legacy-plaintext-sync-source")).await.unwrap(); + let source = + crate::persistence::test_storage::open(&temp_db_path("legacy-plaintext-sync-source")).await.unwrap(); source.save_connections(&[postgres_connection("legacy", "unused")]).await.unwrap(); let mut snapshot = build_sync_snapshot(&source, "legacy-version", None, None).await.unwrap(); snapshot.schema_version = LEGACY_SNAPSHOT_SCHEMA_VERSION; snapshot.connections[0].password = "legacy-password".to_string(); snapshot.connections[0].init_script = Some("CREATE SECRET legacy".to_string()); - let target = Storage::open(&temp_db_path("legacy-plaintext-sync-target")).await.unwrap(); + let target = + crate::persistence::test_storage::open(&temp_db_path("legacy-plaintext-sync-target")).await.unwrap(); apply_sync_snapshot( &target, &snapshot, @@ -4066,7 +4084,8 @@ mod tests { assert_eq!(target.get_secret("legacy", "password").await.unwrap().as_deref(), Some("legacy-password")); assert_eq!(target.get_secret("legacy", "init_script").await.unwrap().as_deref(), Some("CREATE SECRET legacy")); - let metadata_only_target = Storage::open(&temp_db_path("legacy-plaintext-sync-metadata-only")).await.unwrap(); + let metadata_only_target = + crate::persistence::test_storage::open(&temp_db_path("legacy-plaintext-sync-metadata-only")).await.unwrap(); apply_sync_snapshot( &metadata_only_target, &snapshot, @@ -4080,13 +4099,17 @@ mod tests { #[tokio::test] async fn metadata_only_sync_removes_existing_password_when_connection_disables_saving() { - let source = Storage::open(&temp_db_path("sync-no-save-password-metadata-source")).await.unwrap(); + let source = crate::persistence::test_storage::open(&temp_db_path("sync-no-save-password-metadata-source")) + .await + .unwrap(); let mut source_connection = postgres_connection("pg", "unused"); source_connection.save_password = false; source.save_connections(&[source_connection]).await.unwrap(); let snapshot = build_sync_snapshot(&source, "test-version", None, None).await.unwrap(); - let target = Storage::open(&temp_db_path("sync-no-save-password-metadata-target")).await.unwrap(); + let target = crate::persistence::test_storage::open(&temp_db_path("sync-no-save-password-metadata-target")) + .await + .unwrap(); target.save_connections(&[postgres_connection("pg", "local-secret")]).await.unwrap(); apply_sync_snapshot(&target, &snapshot, ApplySnapshotOptions::default()).await.unwrap(); @@ -4096,7 +4119,7 @@ mod tests { #[tokio::test] async fn saved_sync_passphrase_encrypts_nacos_auth_password_without_exposing_it() { - let storage = Storage::open(&temp_db_path("saved-sync-nacos-snapshot")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("saved-sync-nacos-snapshot")).await.unwrap(); storage.save_connections(&[nacos_connection("nacos", "nacos-secret")]).await.unwrap(); save_webdav_sync_secrets_preference(&storage, true, Some("sync-pass")).await.unwrap(); @@ -4115,7 +4138,7 @@ mod tests { #[tokio::test] async fn sync_never_snapshots_or_restores_nacos_passwords_when_saving_is_disabled() { - let source = Storage::open(&temp_db_path("sync-no-save-nacos-source")).await.unwrap(); + let source = crate::persistence::test_storage::open(&temp_db_path("sync-no-save-nacos-source")).await.unwrap(); let mut config = nacos_connection("nacos", "transient-secret"); config.save_password = false; let payload = build_sensitive_payload(&source, std::slice::from_ref(&config), &[]).await.unwrap(); @@ -4150,7 +4173,7 @@ mod tests { #[tokio::test] async fn sync_snapshot_round_trips_tunnel_profiles() { - let storage = Storage::open(&temp_db_path("tunnel-profiles-src")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("tunnel-profiles-src")).await.unwrap(); let profile = TransportLayerConfig::Ssh(SshTunnelConfig { id: "profile-1".to_string(), name: "Bastion".to_string(), @@ -4179,7 +4202,7 @@ mod tests { assert!(!public_json.contains("tunnel-secret")); // Applying with the passphrase restores the full profile on the target. - let target = Storage::open(&temp_db_path("tunnel-profiles-dst")).await.unwrap(); + let target = crate::persistence::test_storage::open(&temp_db_path("tunnel-profiles-dst")).await.unwrap(); apply_sync_snapshot( &target, &snapshot, @@ -4194,7 +4217,7 @@ mod tests { #[tokio::test] async fn sensitive_payload_ai_configs_none_falls_through_to_legacy() { - let storage = Storage::open(&temp_db_path("ai-cfg-none")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("ai-cfg-none")).await.unwrap(); // No ai_configs in payload — fall through to ai_config (legacy) branch let payload = SensitiveSyncPayload { @@ -4212,7 +4235,7 @@ mod tests { #[tokio::test] async fn sensitive_payload_ai_configs_empty_clears_table() { - let storage = Storage::open(&temp_db_path("ai-cfg-empty")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("ai-cfg-empty")).await.unwrap(); // Pre-populate with a config let cfg = make_test_config("to-be-cleared", true); @@ -4234,7 +4257,7 @@ mod tests { #[tokio::test] async fn sensitive_payload_ai_configs_some_saves_configs() { - let storage = Storage::open(&temp_db_path("ai-cfg-some")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("ai-cfg-some")).await.unwrap(); let mut cfg = make_test_config("synced", true); cfg.config.provider = crate::ai::AiProvider::OpenCodeCli; @@ -4274,7 +4297,7 @@ mod tests { #[tokio::test] async fn sensitive_payload_legacy_ai_config_replaces_local_configs_with_same_name() { - let storage = Storage::open(&temp_db_path("ai-cfg-legacy-replace")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&temp_db_path("ai-cfg-legacy-replace")).await.unwrap(); storage.save_ai_config_item(&make_test_config("openai", true)).await.unwrap(); storage.save_ai_config_item(&make_test_config("local-only", false)).await.unwrap(); diff --git a/crates/dbx-core/src/persistence/config/governance.rs b/crates/dbx-core/src/persistence/config/governance.rs index afc336cc99..edb10baa48 100644 --- a/crates/dbx-core/src/persistence/config/governance.rs +++ b/crates/dbx-core/src/persistence/config/governance.rs @@ -477,7 +477,7 @@ mod tests { let tmp = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string()); std::fs::create_dir_all(&tmp).unwrap(); let db_path = tmp.join("test_governance.db"); - let storage = Arc::new(Storage::open(&db_path).await.unwrap()); + let storage = Arc::new(crate::persistence::test_storage::open(&db_path).await.unwrap()); std::mem::forget(tmp); storage } @@ -972,7 +972,7 @@ mod tests { std::fs::create_dir_all(&tmp).unwrap(); let db_path = tmp.join("test_snapshot.db"); let rt = tokio::runtime::Runtime::new().unwrap(); - let storage = Arc::new(rt.block_on(Storage::open(&db_path)).unwrap()); + let storage = Arc::new(rt.block_on(crate::persistence::test_storage::open(&db_path)).unwrap()); std::mem::forget(tmp); storage } diff --git a/crates/dbx-core/src/persistence/mod.rs b/crates/dbx-core/src/persistence/mod.rs index 3f31b17e99..42ff8df5a8 100644 --- a/crates/dbx-core/src/persistence/mod.rs +++ b/crates/dbx-core/src/persistence/mod.rs @@ -5,3 +5,5 @@ pub mod saved_sql; pub mod secret_codec; pub mod state_persistence; pub mod storage; +#[cfg(any(test, feature = "test-support"))] +pub mod test_storage; diff --git a/crates/dbx-core/src/persistence/secret_codec.rs b/crates/dbx-core/src/persistence/secret_codec.rs index f59470b761..b86e9e87c5 100644 --- a/crates/dbx-core/src/persistence/secret_codec.rs +++ b/crates/dbx-core/src/persistence/secret_codec.rs @@ -34,6 +34,8 @@ pub enum SecretKeyPolicy { PlatformDefault, ManagedDataDir, ExternalOnly, + #[cfg(any(test, feature = "test-support"))] + TestDataDir, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -111,6 +113,10 @@ impl SecretCodec { where F: Fn(&str) -> Option, { + #[cfg(any(test, feature = "test-support"))] + if matches!(policy, SecretKeyPolicy::TestDataDir) { + return Self::resolve_managed_data_dir(data_dir, allow_create); + } if let Some(path) = env_lookup("DBX_SECRET_KEY_FILE") { let path = std::path::PathBuf::from(path); let codec = Self::read_key_file(&path, false)?; @@ -123,34 +129,36 @@ impl SecretCodec { } match policy { - SecretKeyPolicy::ManagedDataDir => { - let path = managed_key_path(data_dir); - match path.symlink_metadata() { - Ok(metadata) if metadata.file_type().is_symlink() => Err("KEY_FILE_UNAVAILABLE".to_string()), - Ok(_) => { - if allow_create { - secure_managed_key_permissions(&path)?; - } - let codec = read_key_file_with_retry(&path, true)?; - Ok(SecretKeyResolution { codec, source: SecretKeySource::ManagedDataDir }) - } - Err(error) if error.kind() == std::io::ErrorKind::NotFound && allow_create => { - let codec = create_managed_key(&path)?; - Ok(SecretKeyResolution { codec, source: SecretKeySource::ManagedDataDir }) - } - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - Err("MISSING_MANAGED_KEY".to_string()) - } - Err(_) => Err("KEY_FILE_UNAVAILABLE".to_string()), - } - } + SecretKeyPolicy::ManagedDataDir => Self::resolve_managed_data_dir(data_dir, allow_create), SecretKeyPolicy::ExternalOnly => Err("MISSING_EXTERNAL_KEY".to_string()), + #[cfg(any(test, feature = "test-support"))] + SecretKeyPolicy::TestDataDir => Self::resolve_managed_data_dir(data_dir, allow_create), SecretKeyPolicy::PlatformDefault => { Self::resolve_platform_default(default_key_path(), allow_create, platform_keyring_codec) } } } + fn resolve_managed_data_dir(data_dir: &std::path::Path, allow_create: bool) -> Result { + let path = managed_key_path(data_dir); + match path.symlink_metadata() { + Ok(metadata) if metadata.file_type().is_symlink() => Err("KEY_FILE_UNAVAILABLE".to_string()), + Ok(_) => { + if allow_create { + secure_managed_key_permissions(&path)?; + } + let codec = read_key_file_with_retry(&path, true)?; + Ok(SecretKeyResolution { codec, source: SecretKeySource::ManagedDataDir }) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound && allow_create => { + let codec = create_managed_key(&path)?; + Ok(SecretKeyResolution { codec, source: SecretKeySource::ManagedDataDir }) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Err("MISSING_MANAGED_KEY".to_string()), + Err(_) => Err("KEY_FILE_UNAVAILABLE".to_string()), + } + } + fn resolve_platform_default( compatibility_path: Option, allow_create: bool, @@ -514,6 +522,18 @@ fn aad(namespace: &str, key: &str) -> String { #[cfg(test)] mod tests { + #[test] + fn isolated_test_policy_never_reads_process_credentials() { + let directory = tempfile::tempdir().unwrap(); + let resolved = + super::SecretCodec::resolve_with_env(super::SecretKeyPolicy::TestDataDir, directory.path(), true, |_| { + panic!("test storage must not inspect process credential overrides") + }) + .unwrap(); + assert_eq!(resolved.source, super::SecretKeySource::ManagedDataDir); + assert!(super::managed_key_path(directory.path()).is_file()); + } + use super::{ managed_key_path, read_key_file_with_retry, run_secret_service_operation, SecretCodec, SecretKeyPolicy, SecretKeySource, diff --git a/crates/dbx-core/src/persistence/state_persistence.rs b/crates/dbx-core/src/persistence/state_persistence.rs index 7d8695ed2f..2723ca32fd 100644 --- a/crates/dbx-core/src/persistence/state_persistence.rs +++ b/crates/dbx-core/src/persistence/state_persistence.rs @@ -933,7 +933,7 @@ mod tests { async fn create_local_backend() -> LocalBackend { let path = temp_db_path("local-backend"); - let storage = Arc::new(Storage::open(&path).await.unwrap()); + let storage = Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); LocalBackend::new(storage) } @@ -1315,7 +1315,7 @@ mod tests { #[tokio::test] async fn storage_compare_and_swap_new_key() { let path = temp_db_path("cas-new-key"); - let storage = Arc::new(Storage::open(&path).await.unwrap()); + let storage = Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); let success = storage.compare_and_swap_state("cas-new", None, b"data", "text/plain").await.unwrap(); assert!(success); let (data, _) = storage.load_state("cas-new").await.unwrap().unwrap(); @@ -1325,7 +1325,7 @@ mod tests { #[tokio::test] async fn storage_compare_and_swap_exact_version() { let path = temp_db_path("cas-exact"); - let storage = Arc::new(Storage::open(&path).await.unwrap()); + let storage = Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); storage.save_state("cas-key", b"v1", "text/plain").await.unwrap(); let version = storage.get_state_version("cas-key").await.unwrap().unwrap(); assert_eq!(version, 1); @@ -1339,7 +1339,7 @@ mod tests { #[tokio::test] async fn storage_compare_and_swap_wrong_version() { let path = temp_db_path("cas-wrong-ver"); - let storage = Arc::new(Storage::open(&path).await.unwrap()); + let storage = Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); storage.save_state("cas-key", b"v1", "text/plain").await.unwrap(); let success = storage.compare_and_swap_state("cas-key", Some(999), b"v2", "text/plain").await.unwrap(); assert!(!success); diff --git a/crates/dbx-core/src/persistence/storage.rs b/crates/dbx-core/src/persistence/storage.rs index 6ad70ac330..13da1da810 100644 --- a/crates/dbx-core/src/persistence/storage.rs +++ b/crates/dbx-core/src/persistence/storage.rs @@ -1150,7 +1150,11 @@ fn file_digest(path: &Path) -> Result { impl Storage { pub async fn open(db_path: &Path) -> Result { - let storage = Self::open_unmigrated(db_path).await?; + Self::open_with_secret_key_policy(db_path, SecretKeyPolicy::PlatformDefault).await + } + + pub async fn open_with_secret_key_policy(db_path: &Path, policy: SecretKeyPolicy) -> Result { + let storage = Self::open_unmigrated(db_path).await?.with_secret_key_policy(policy); let preflight = storage.inspect_data_migration().await?; if preflight.needs_migration && !preflight.key_provider_available && !preflight.key_creation_allowed { return Err(preflight.error_code.unwrap_or_else(|| "KEY_PROVIDER_UNAVAILABLE".to_string())); @@ -1431,7 +1435,7 @@ impl Storage { .as_ref() .err() .is_some_and(|error| matches!(error.as_str(), "MISSING_MANAGED_KEY" | "KEY_PROVIDER_UNAVAILABLE")) - && matches!(self.secret_key_policy, SecretKeyPolicy::PlatformDefault | SecretKeyPolicy::ManagedDataDir) + && !matches!(self.secret_key_policy, SecretKeyPolicy::ExternalOnly) && encrypted == 0 && (database_plaintext_count > 0 || sync_credentials > 0 || files.iter().any(|file| file.exists)); let mut key_error_code = key_probe.as_ref().err().cloned(); @@ -8290,7 +8294,7 @@ mod tests { async fn migration_scan_cache_recovers_legacy_success_with_stale_counts() { let dir = temp_data_dir("migration-stale-cache"); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open_unmigrated(&dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open_unmigrated(&dir.join("dbx.db")).await.unwrap(); storage.set_migration_state(super::MigrationState::Succeeded, None, None, None, None).await.unwrap(); storage .with_conn(|conn| { @@ -8316,7 +8320,7 @@ mod tests { async fn migration_preflight_is_read_only() { let dir = temp_data_dir("migration-cache-transition"); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open_unmigrated(&dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open_unmigrated(&dir.join("dbx.db")).await.unwrap(); storage.inspect_data_migration().await.unwrap(); let before = storage.load_migration_state().await.unwrap(); assert_eq!(before.state, super::MigrationState::Pending); @@ -8348,7 +8352,7 @@ mod tests { async fn managed_data_dir_key_is_created_only_when_plaintext_migration_starts() { let dir = tempfile::tempdir().unwrap(); let database_path = dir.path().join("dbx.db"); - let storage = Storage::open_unmigrated(&database_path) + let storage = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8388,7 +8392,7 @@ mod tests { assert!(row.1.starts_with("dbxenc1.")); drop(storage); - let reopened = Storage::open_unmigrated(&database_path) + let reopened = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8398,7 +8402,7 @@ mod tests { #[tokio::test] async fn secret_migration_write_failure_rolls_back_all_rows() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open_unmigrated(&directory.path().join("dbx.db")) + let storage = crate::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8433,7 +8437,7 @@ mod tests { async fn secret_migration_late_failure_restores_backup_and_retries_after_reopen() { let directory = tempfile::tempdir().unwrap(); let database_path = directory.path().join("dbx.db"); - let storage = Storage::open_unmigrated(&database_path) + let storage = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8473,7 +8477,7 @@ mod tests { drop(storage); std::fs::write(&legacy_path, "[]").unwrap(); - let reopened = Storage::open_unmigrated(&database_path) + let reopened = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8488,7 +8492,7 @@ mod tests { async fn secret_migration_running_state_resumes_after_reopen() { let directory = tempfile::tempdir().unwrap(); let database_path = directory.path().join("dbx.db"); - let storage = Storage::open_unmigrated(&database_path) + let storage = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8496,7 +8500,7 @@ mod tests { let backup = storage.create_migration_backup().await.unwrap(); storage.set_migration_state(super::MigrationState::Running, Some(&backup), None, None, None).await.unwrap(); drop(storage); - let reopened = Storage::open_unmigrated(&database_path) + let reopened = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8511,7 +8515,7 @@ mod tests { #[tokio::test] async fn secret_migration_rejects_wrong_or_invalid_managed_keys_without_replacing_them() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open_unmigrated(&directory.path().join("dbx.db")) + let storage = crate::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8537,7 +8541,7 @@ mod tests { async fn missing_managed_key_for_existing_ciphertext_never_creates_a_replacement() { let dir = tempfile::tempdir().unwrap(); let database_path = dir.path().join("dbx.db"); - let storage = Storage::open_unmigrated(&database_path) + let storage = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8557,7 +8561,7 @@ mod tests { #[tokio::test] async fn secret_write_after_managed_key_loss_never_creates_a_replacement() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open_unmigrated(&directory.path().join("dbx.db")) + let storage = crate::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8582,7 +8586,7 @@ mod tests { #[tokio::test] async fn connection_write_after_managed_key_loss_preserves_existing_ciphertext() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open_unmigrated(&directory.path().join("dbx.db")) + let storage = crate::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -8607,7 +8611,7 @@ mod tests { async fn disabled_secret_key_creation_does_not_provision_on_write() { let dir = tempfile::tempdir().unwrap(); let database_path = dir.path().join("dbx.db"); - let storage = Storage::open_unmigrated(&database_path) + let storage = crate::persistence::test_storage::open_unmigrated(&database_path) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir) @@ -8633,7 +8637,7 @@ mod tests { #[tokio::test] async fn delete_table_vgroups_for_connection_scopes_by_prefix() { let path = temp_db_path("vgroup-delete"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let layout = serde_json::json!({ "version": 1, "groups": [], "order": [] }); // 真实 scope_key 形如 `{connectionId}\u{0}{linkedServer}\u{0}{catalog}\u{0}{database}\u{0}{schema}`。 @@ -8676,7 +8680,7 @@ mod tests { let dir = temp_data_dir_with_mode("permission-single-user", 0o755); let path = dir.join("dbx.db"); - drop(Storage::open(&path).await.unwrap()); + drop(crate::persistence::test_storage::open(&path).await.unwrap()); // Stand in for an installation created before this hardening existed, // including a rollback journal left behind by an interrupted write. @@ -8686,7 +8690,7 @@ mod tests { std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o644)).unwrap(); } - drop(Storage::open(&path).await.unwrap()); + drop(crate::persistence::test_storage::open(&path).await.unwrap()); for name in ["dbx.db", "dbx.db-journal", "dbx.db-wal", "dbx.db-shm"] { let Some(mode) = file_mode(&dir.join(name)) else { continue }; @@ -8707,7 +8711,7 @@ mod tests { // data directory. let dir = temp_data_dir_with_mode("permission-group-shared", 0o770); let path = dir.join("dbx.db"); - drop(Storage::open(&path).await.unwrap()); + drop(crate::persistence::test_storage::open(&path).await.unwrap()); let journal = dir.join("dbx.db-journal"); std::fs::write(&journal, b"").unwrap(); @@ -8715,7 +8719,7 @@ mod tests { std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o664)).unwrap(); } - drop(Storage::open(&path).await.unwrap()); + drop(crate::persistence::test_storage::open(&path).await.unwrap()); for name in ["dbx.db", "dbx.db-journal"] { let Some(mode) = file_mode(&dir.join(name)) else { continue }; @@ -8733,13 +8737,13 @@ mod tests { let dir = temp_data_dir_with_mode("permission-init-failure", 0o755); let path = dir.join("dbx.db"); - drop(Storage::open(&path).await.unwrap()); + drop(crate::persistence::test_storage::open(&path).await.unwrap()); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap(); // A read-only directory keeps the database file itself writable while // denying the journal SQLite needs, so the schema pass fails. std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o500)).unwrap(); - let result = Storage::open(&path).await; + let result = crate::persistence::test_storage::open(&path).await; std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap(); assert!(result.is_err(), "expected schema initialization to fail on a read-only directory"); @@ -8828,7 +8832,7 @@ mod tests { #[tokio::test] async fn ai_conversation_soft_cap_never_evicts_protected_runs() { let path = temp_db_path("ai-conversation-soft-cap"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let protected = ai_conversation("protected", "0000"); let protected_run = ai_run("protected-run", "protected", AiRunStatus::Running, "0000"); @@ -8850,7 +8854,7 @@ mod tests { #[tokio::test] async fn ai_conversation_soft_cap_allows_more_than_fifty_protected_runs() { let path = temp_db_path("ai-conversation-protected-overflow"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); for index in 0..51 { let id = format!("protected-{index}"); @@ -8875,7 +8879,7 @@ mod tests { #[tokio::test] async fn ai_conversation_soft_cap_protects_pending_recoverable_runs() { let path = temp_db_path("ai-conversation-pending-recoverable-protection"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); // A recovered pending-input run (PRD §7 line 93) must be protected like // any other non-terminal run: its draft is not lost to pruning. @@ -8899,7 +8903,7 @@ mod tests { #[tokio::test] async fn ai_run_roundtrips_fifo_category_and_pending_input() { let path = temp_db_path("ai-run-fifo-category-roundtrip"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let conversation = ai_conversation("fifo-conv", "0000"); let mut run = ai_run("fifo-run", "fifo-conv", AiRunStatus::Queued, "0000"); @@ -8931,7 +8935,7 @@ mod tests { #[tokio::test] async fn saving_a_conversation_keeps_its_background_runs() { let path = temp_db_path("ai-conversation-upsert-keeps-runs"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); // `ai_runs` declares an ON DELETE CASCADE relationship. Exercise the // snapshot path with enforcement enabled so an accidental REPLACE // (delete + insert) cannot silently erase an active run on restart. @@ -8966,7 +8970,7 @@ mod tests { // conversation (keeping the newest few, which drive the row status // badge after restart) and never touches recovery-relevant runs. let path = temp_db_path("ai-terminal-runs-capped"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); // A non-terminal run must always survive - it is the recovery payload. let conversation = ai_conversation("cap-conv", "0000"); @@ -9010,7 +9014,7 @@ mod tests { ) .unwrap(); drop(conn); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut loaded = storage.load_ai_conversations().await.unwrap(); assert_eq!(loaded[0].title, "SQL conversation"); assert!(loaded[0].plugin_context.is_none()); @@ -9023,7 +9027,7 @@ mod tests { #[tokio::test] async fn ai_conversation_retains_plugin_snapshot_without_a_database() { let path = temp_db_path("ai-plugin-conversation"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut conversation = ai_conversation("market-analysis", "0000"); conversation.database.clear(); let snapshot = serde_json::json!({ @@ -9054,7 +9058,7 @@ mod tests { #[tokio::test] async fn ai_conversation_roundtrips_queued_input() { let path = temp_db_path("ai-conversation-queued-input-roundtrip"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut conversation = ai_conversation("queued-conv", "0000"); conversation.queued_input = Some("run this after the current task".to_string()); @@ -9095,7 +9099,7 @@ mod tests { #[tokio::test] async fn ai_conversation_roundtrips_connection_binding() { let path = temp_db_path("ai-conversation-binding-roundtrip"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut conversation = ai_conversation("bound-conv", "0000"); conversation.connection_id = "conn-prod".to_string(); @@ -9130,7 +9134,7 @@ mod tests { "INSERT INTO connections (id, config_json) VALUES ('c-prod', '{\"id\":\"c-prod\",\"name\":\"Prod MySQL\",\"db_type\":\"mysql\",\"host\":\"127.0.0.1\",\"port\":3306,\"username\":\"u\",\"password\":\"p\",\"database\":null}');", ); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let loaded = storage.load_ai_conversations().await.unwrap(); let conversation = |id: &str| loaded.iter().find(|item| item.id == id).unwrap(); @@ -9155,7 +9159,7 @@ mod tests { INSERT INTO connections (id, config_json) VALUES ('c2', '{\"id\":\"c2\",\"name\":\"Shared Name\",\"db_type\":\"mysql\",\"host\":\"127.0.0.1\",\"port\":3306,\"username\":\"u\",\"password\":\"p\",\"database\":null}');", ); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); for conversation in storage.load_ai_conversations().await.unwrap() { // Two connections share the name (names are not unique), and an empty // name identifies nothing: neither may be auto-bound. @@ -9168,7 +9172,7 @@ mod tests { #[tokio::test] async fn ai_conversation_backfill_never_overwrites_an_existing_binding() { let path = temp_db_path("ai-conversation-binding-idempotent"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut conversation = ai_conversation("pinned", "0000"); conversation.connection_id = "conn-a".to_string(); storage.save_ai_conversation(&conversation).await.unwrap(); @@ -9181,7 +9185,7 @@ mod tests { .unwrap(); drop(conn); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let loaded = storage.load_ai_conversations().await.unwrap(); assert_eq!(loaded[0].connection_id, "conn-a"); @@ -9207,7 +9211,7 @@ mod tests { async fn history_retention_uses_persisted_limit_on_the_next_write() { for (limit, expected) in [(200, 200), (5000, 1002), (10000, 1002), (0, 1002)] { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("dbx.db")).await.unwrap(); seed_history_backlog(&storage, 1001).await; storage .with_conn(move |conn| { @@ -9248,7 +9252,7 @@ mod tests { async fn history_retention_defaults_validates_and_survives_stale_settings() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("dbx.db"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(storage.load_history_retention_limit().await.unwrap(), 1000); let stale = storage.load_app_settings_json().await.unwrap(); for limit in [200, 1000, 5000, 10000, 0] { @@ -9262,14 +9266,14 @@ mod tests { storage.save_app_settings_json(&stale).await.unwrap(); assert_eq!(storage.load_history_retention_limit().await.unwrap(), 0); drop(storage); - let reopened = Storage::open(&path).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(reopened.load_history_retention_limit().await.unwrap(), 0); } #[tokio::test] async fn history_retention_invalid_persisted_values_use_default() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("dbx.db")).await.unwrap(); for value in [ serde_json::json!(-1), serde_json::json!(1), @@ -9309,7 +9313,7 @@ mod tests { #[tokio::test] async fn history_retention_setting_changes_do_not_prune_until_next_write() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("dbx.db")).await.unwrap(); storage.save_history_retention_limit(0).await.unwrap(); seed_history_backlog(&storage, 1001).await; storage.save_history_retention_limit(200).await.unwrap(); @@ -9332,7 +9336,7 @@ mod tests { #[tokio::test] async fn history_search_filters_connection_database_and_legacy_entries() { let path = temp_db_path("history-search-scope"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let entries = [ history_entry("1", "conn-a", "Primary", "sales", "select 1", "2026-07-18T01:00:00Z", true), history_entry("2", "conn-b", "Replica", "sales", "select 2", "2026-07-18T02:00:00Z", true), @@ -9384,7 +9388,7 @@ mod tests { #[tokio::test] async fn history_search_combines_whole_connections_with_narrowed_database_scopes() { let path = temp_db_path("history-search-hierarchical-scope"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let entries = [ history_entry("a-sales", "conn-a", "Primary", "sales", "select 1", "2026-07-18T01:00:00Z", true), history_entry("a-archive", "conn-a", "Primary", "archive", "select 2", "2026-07-18T02:00:00Z", true), @@ -9429,7 +9433,7 @@ mod tests { #[tokio::test] async fn history_search_combines_text_status_and_time_filters() { let path = temp_db_path("history-search-fields"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let entries = [ history_entry("1", "conn", "Main", "app", "select 100% from orders", "2026-07-17T23:59:59Z", false), history_entry("2", "conn", "Main", "app", "select 1000 from orders", "2026-07-18T12:00:00Z", false), @@ -9470,7 +9474,7 @@ mod tests { #[tokio::test] async fn history_search_cursor_is_stable_for_equal_timestamps() { let path = temp_db_path("history-search-cursor"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); for id in ["a", "b", "c"] { storage .save_history_entry(&history_entry(id, "conn", "Main", "app", "select 1", "2026-07-18T12:00:00Z", true)) @@ -9514,7 +9518,7 @@ mod tests { #[tokio::test] async fn tunnel_profiles_roundtrip_and_preserve_secrets() { let path = temp_db_path("tunnel-profiles"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let profile = ssh_profile("profile-1", "s3cret"); storage.save_tunnel_profiles(std::slice::from_ref(&profile)).await.unwrap(); @@ -9542,7 +9546,7 @@ mod tests { #[tokio::test] async fn tunnel_profile_secret_fields_are_not_written_to_config_json() { let path = temp_db_path("tunnel-secret-at-rest"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let profile = TransportLayerConfig::HttpTunnel(HttpTunnelConfig { id: "http-secret".to_string(), name: "HTTP".to_string(), @@ -9582,7 +9586,7 @@ mod tests { #[tokio::test] async fn tunnel_profiles_reject_empty_ids() { let path = temp_db_path("tunnel-profiles-empty-id"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let profile = ssh_profile("", "secret"); assert!(storage.save_tunnel_profiles(&[profile]).await.is_err()); @@ -9624,7 +9628,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_cassandra_tls_passwords_to_secret_table_and_restores_them() { let path = temp_db_path("cassandra-tls-secrets"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_connections(&[cassandra_connection("cassandra")]).await.unwrap(); @@ -9658,7 +9662,7 @@ mod tests { #[tokio::test] async fn save_connections_does_not_persist_password_when_save_password_false() { let path = temp_db_path("save-password-false"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = plain_connection("no-save", "hunter2"); config.save_password = false; @@ -9682,8 +9686,8 @@ mod tests { // the same inter-connection SQLite file locking that separate OS // processes would hit. let path = temp_db_path("concurrent-save-connections"); - let storage_a = std::sync::Arc::new(Storage::open(&path).await.unwrap()); - let storage_b = std::sync::Arc::new(Storage::open(&path).await.unwrap()); + let storage_a = std::sync::Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); + let storage_b = std::sync::Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); let mut tasks = Vec::new(); for i in 0..20 { @@ -9702,7 +9706,7 @@ mod tests { #[tokio::test] async fn save_connections_persists_password_when_save_password_true() { let path = temp_db_path("save-password-true"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let config = plain_connection("save-yes", "hunter2"); storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); @@ -9718,7 +9722,7 @@ mod tests { #[tokio::test] async fn plugin_secret_reads_require_a_key_only_for_ciphertext() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open_unmigrated(&dir.path().join("dbx.db")) + let storage = crate::persistence::test_storage::open_unmigrated(&dir.path().join("dbx.db")) .await .unwrap() .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); @@ -9743,7 +9747,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_plugin_secrets_to_secret_table_and_clears_removed_values() { let path = temp_db_path("plugin-secrets"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = plain_connection("plugin", ""); config.db_type = DatabaseType::Plugin; config.plugin_id = Some("example.plugin".to_string()); @@ -9778,7 +9782,7 @@ mod tests { #[tokio::test] async fn switching_save_password_off_removes_stored_password() { let path = temp_db_path("save-password-switch-off"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = plain_connection("switch", "hunter2"); storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); @@ -9999,7 +10003,7 @@ mod tests { async fn create_data_dir_with_connection(name: &str, connection_id: &str, token: &str) -> std::path::PathBuf { let data_dir = temp_data_dir(name); - let storage = Storage::open(&data_dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&data_dir.join("dbx.db")).await.unwrap(); storage.save_connections(&[mq_connection(connection_id, token)]).await.unwrap(); drop(storage); data_dir @@ -10009,11 +10013,13 @@ mod tests { async fn import_user_data_db_copies_source_when_target_is_missing() { let source_dir = create_data_dir_with_connection("import-source", "source-connection", "source-token").await; let target_dir = temp_data_dir("import-target"); + std::fs::create_dir_all(managed_key_path(&target_dir).parent().unwrap()).unwrap(); + std::fs::copy(managed_key_path(&source_dir), managed_key_path(&target_dir)).unwrap(); let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); assert_eq!(result, DataDbImportResult::Imported); - let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); let connections = storage.load_connections().await.unwrap(); assert_eq!(connections.len(), 1); assert_eq!(connections[0].id, "source-connection"); @@ -10030,7 +10036,7 @@ mod tests { let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); assert_eq!(result, DataDbImportResult::SkippedTargetHasData); - let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); let connections = storage.load_connections().await.unwrap(); assert_eq!(connections.len(), 1); assert_eq!(connections[0].id, "target-connection"); @@ -10040,7 +10046,7 @@ mod tests { #[tokio::test] async fn import_user_data_db_recognizes_settings_and_snippets_as_user_data() { let source_dir = temp_data_dir("import-settings-only-source"); - let source_storage = Storage::open(&source_dir.join("dbx.db")).await.unwrap(); + let source_storage = crate::persistence::test_storage::open(&source_dir.join("dbx.db")).await.unwrap(); source_storage .save_desktop_settings(&DesktopSettings { debug_logging_enabled: true, ..DesktopSettings::default() }) .await @@ -10057,7 +10063,7 @@ mod tests { let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); assert_eq!(result, DataDbImportResult::Imported); - let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); assert!(storage.load_desktop_settings().await.unwrap().debug_logging_enabled); assert_eq!(storage.load_editor_settings().await.unwrap().unwrap()["snippets"][0]["body"], "SELECT 42"); } @@ -10067,7 +10073,7 @@ mod tests { let source_dir = create_data_dir_with_connection("import-source-settings-target", "source-connection", "source-token").await; let target_dir = temp_data_dir("import-target-settings-only"); - let target_storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let target_storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); target_storage .save_editor_settings(&serde_json::json!({ "snippets": [{ "id": "target", "prefix": "tgt", "body": "SELECT 7" }] @@ -10079,7 +10085,7 @@ mod tests { let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); assert_eq!(result, DataDbImportResult::SkippedTargetHasData); - let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); assert_eq!(storage.load_editor_settings().await.unwrap().unwrap()["snippets"][0]["body"], "SELECT 7"); } @@ -10088,12 +10094,14 @@ mod tests { let source_dir = create_data_dir_with_connection("import-source-empty-target", "source-connection", "source-token").await; let target_dir = temp_data_dir("import-empty-target"); - let _target_storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + std::fs::create_dir_all(managed_key_path(&target_dir).parent().unwrap()).unwrap(); + std::fs::copy(managed_key_path(&source_dir), managed_key_path(&target_dir)).unwrap(); + let _target_storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); let result = maybe_import_user_data_db(&target_dir, Some(&source_dir)).unwrap(); assert_eq!(result, DataDbImportResult::Imported); - let storage = Storage::open(&target_dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&target_dir.join("dbx.db")).await.unwrap(); let connections = storage.load_connections().await.unwrap(); assert_eq!(connections.len(), 1); assert_eq!(connections[0].id, "source-connection"); @@ -10102,7 +10110,7 @@ mod tests { #[tokio::test] async fn import_user_data_db_skips_empty_source_schema() { let source_dir = temp_data_dir("import-empty-source"); - let source_storage = Storage::open(&source_dir.join("dbx.db")).await.unwrap(); + let source_storage = crate::persistence::test_storage::open(&source_dir.join("dbx.db")).await.unwrap(); drop(source_storage); let target_dir = temp_data_dir("import-empty-source-target"); @@ -10128,7 +10136,7 @@ mod tests { #[tokio::test] async fn save_connections_preserves_database_info() { let path = temp_db_path("database-info"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = mq_connection("database-info", "mq-secret"); config.database_info = Some(DatabaseConnectionInfo { product_name: Some("MySQL".to_string()), @@ -10160,7 +10168,7 @@ mod tests { #[tokio::test] async fn save_connection_mqtt_saved_topics_updates_only_target_and_preserves_secrets() { let path = temp_db_path("mqtt-saved-topics"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let target = mq_connection("target", "target-secret"); let untouched = mq_connection("untouched", "untouched-secret"); storage.save_connections(&[target.clone(), untouched.clone()]).await.unwrap(); @@ -10187,7 +10195,7 @@ mod tests { #[tokio::test] async fn save_connection_mqtt_saved_topics_rejects_non_object_external_config() { let path = temp_db_path("mqtt-saved-topics-invalid"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut target = mq_connection("target", "target-secret"); target.external_config = Some(serde_json::json!("invalid")); storage.save_connections(std::slice::from_ref(&target)).await.unwrap(); @@ -10200,7 +10208,7 @@ mod tests { #[tokio::test] async fn save_connection_driver_profile_updates_only_the_target_metadata() { let path = temp_db_path("connection-driver-profile"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let target = mq_connection("target", "target-secret"); let untouched = mq_connection("untouched", "untouched-secret"); storage.save_connections(&[target.clone(), untouched.clone()]).await.unwrap(); @@ -10239,7 +10247,7 @@ mod tests { #[tokio::test] async fn save_connection_database_updates_only_the_target_and_preserves_secrets() { let path = temp_db_path("connection-database"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let target = mq_connection("target", "target-secret"); let untouched = mq_connection("untouched", "untouched-secret"); storage.save_connections(&[target.clone(), untouched.clone()]).await.unwrap(); @@ -10259,7 +10267,7 @@ mod tests { #[tokio::test] async fn save_connection_driver_profile_rejects_a_stale_connection_config() { let path = temp_db_path("connection-driver-profile-stale"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let original = mq_connection("target", "target-secret"); storage.save_connections(std::slice::from_ref(&original)).await.unwrap(); @@ -10286,7 +10294,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_mq_auth_token_to_secret_table_and_restores_it() { let path = temp_db_path("mq-token-secrets"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_connections(&[mq_connection("pulsar", "mq-token-secret")]).await.unwrap(); @@ -10304,7 +10312,7 @@ mod tests { #[tokio::test] async fn unreadable_saved_connections_do_not_block_loading_or_get_deleted_by_list_saves() { let path = temp_db_path("unreadable-connection-preservation"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut known = mq_connection("known", "known-secret"); storage.save_connections(std::slice::from_ref(&known)).await.unwrap(); @@ -10353,7 +10361,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_plugin_secrets_to_secret_table_and_restores_them() { let path = temp_db_path("plugin-connection-secret"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = mq_connection("plugin-connection", ""); config.name = "Hello plugin".to_string(); config.db_type = DatabaseType::Plugin; @@ -10386,7 +10394,7 @@ mod tests { #[tokio::test] async fn load_connections_preserves_opaque_null_plugin_secrets() { let path = temp_db_path("plugin-null-secret"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = mq_connection("plugin-connection", ""); config.name = "SSH plugin".to_string(); config.db_type = DatabaseType::Plugin; @@ -10429,7 +10437,7 @@ mod tests { storage.save_connections(&loaded).await.unwrap(); assert_eq!(storage.load_connections().await.unwrap()[0].connection_secrets, *secrets); drop(storage); - let reopened = Storage::open(&path).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(reopened.load_connections().await.unwrap()[0].connection_secrets, *secrets); let _ = std::fs::remove_file(path); @@ -10438,7 +10446,7 @@ mod tests { #[tokio::test] async fn metadata_save_scrubs_mq_auth_token_and_preserves_existing_secret() { let path = temp_db_path("mq-token-metadata"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let original = mq_connection("pulsar", "existing-token"); storage.save_connections(std::slice::from_ref(&original)).await.unwrap(); @@ -10464,7 +10472,7 @@ mod tests { #[tokio::test] async fn load_connections_migrates_legacy_mq_auth_token_out_of_config_json() { let path = temp_db_path("mq-token-legacy-migration"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); insert_raw_connection(&storage, &mq_connection("pulsar", "legacy-token")).await; let loaded = storage.load_connections().await.unwrap(); @@ -10480,7 +10488,7 @@ mod tests { #[tokio::test] async fn save_connections_deletes_stale_mq_auth_secrets_when_kind_changes() { let path = temp_db_path("mq-auth-kind-change"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_connections(&[mq_connection("pulsar", "old-token")]).await.unwrap(); let mut config = mq_connection("pulsar", ""); config.external_config = Some(serde_json::json!({ @@ -10502,7 +10510,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_mq_token_signing_key_to_secret_table_and_restores_it() { let path = temp_db_path("mq-token-signing-secret"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = mq_connection("pulsar", ""); config.external_config = Some(serde_json::json!({ "systemKind": "pulsar", @@ -10532,7 +10540,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_nacos_auth_password_to_secret_table_and_restores_it() { let path = temp_db_path("nacos-auth-secret"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_connections(&[nacos_connection("nacos", "nacos-secret")]).await.unwrap(); @@ -10553,7 +10561,7 @@ mod tests { #[tokio::test] async fn save_connections_moves_separate_rnacos_console_password_to_secret_table() { let path = temp_db_path("rnacos-console-auth-secret"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = nacos_connection("rnacos", ""); config.external_config = Some(serde_json::json!({ "implementation": "rnacos", @@ -10586,7 +10594,7 @@ mod tests { #[tokio::test] async fn save_connections_does_not_persist_nacos_passwords_when_save_password_is_false() { let path = temp_db_path("nacos-auth-no-save"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = nacos_connection_with_console_auth("nacos", "primary-secret", "console-secret"); config.save_password = false; @@ -10602,7 +10610,7 @@ mod tests { #[tokio::test] async fn switching_nacos_password_saving_off_removes_all_stored_auth_secrets() { let path = temp_db_path("nacos-auth-disable-save"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = nacos_connection_with_console_auth("nacos", "primary-secret", "console-secret"); storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); assert!(storage.get_secret("nacos", NACOS_AUTH_PASSWORD_KEY).await.unwrap().is_some()); @@ -10618,7 +10626,7 @@ mod tests { #[tokio::test] async fn metadata_sync_removes_nacos_auth_secrets_when_password_saving_is_disabled() { let path = temp_db_path("nacos-auth-no-save-metadata-sync"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = nacos_connection_with_console_auth("nacos", "primary-secret", "console-secret"); storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); @@ -10635,7 +10643,7 @@ mod tests { #[tokio::test] async fn load_connections_cleans_legacy_nacos_passwords_when_saving_is_disabled() { let path = temp_db_path("nacos-auth-no-save-legacy-cleanup"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = nacos_connection_with_console_auth("nacos", "legacy-primary-secret", "legacy-console-secret"); config.save_password = false; insert_raw_connection(&storage, &config).await; @@ -10656,7 +10664,7 @@ mod tests { #[tokio::test] async fn load_connections_migrates_legacy_nacos_auth_password_out_of_config_json() { let path = temp_db_path("nacos-auth-legacy-migration"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); insert_raw_connection(&storage, &nacos_connection("nacos", "legacy-nacos-secret")).await; let loaded = storage.load_connections().await.unwrap(); @@ -10675,7 +10683,7 @@ mod tests { #[tokio::test] async fn desktop_settings_default_to_background_enabled() { let path = temp_db_path("desktop-settings-default"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(storage.load_desktop_settings().await.unwrap(), DesktopSettings::default()); } @@ -10683,7 +10691,7 @@ mod tests { #[tokio::test] async fn mcp_global_policy_defaults_unconfigured_and_roundtrips_atomically() { let path = temp_db_path("mcp-global-policy"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!( storage.load_mcp_global_policy().await.unwrap(), @@ -10741,7 +10749,7 @@ mod tests { #[tokio::test] async fn mcp_global_policy_fails_closed_on_malformed_settings() { let path = temp_db_path("mcp-global-policy-malformed"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .with_conn(|conn| { conn.execute( @@ -10761,7 +10769,7 @@ mod tests { #[tokio::test] async fn malformed_app_settings_cannot_be_silently_replaced_by_an_unrelated_save() { let path = temp_db_path("mcp-global-policy-invalid-settings-shape"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .with_conn(|conn| { conn.execute("INSERT OR REPLACE INTO app_settings (id, settings_json) VALUES (1, ?1)", ["[]"]) @@ -10788,7 +10796,7 @@ mod tests { #[tokio::test] async fn mcp_global_policy_defaults_dangerous_sql_to_disabled_for_existing_settings() { let path = temp_db_path("mcp-global-policy-existing"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .with_conn(|conn| { conn.execute( @@ -10810,7 +10818,7 @@ mod tests { #[tokio::test] async fn mcp_connection_mutations_are_atomic_and_recheck_policy() { let path = temp_db_path("mcp-connection-mutation-guard"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let kept = mq_connection("kept", "kept-token"); let removed = mq_connection("removed", "removed-token"); storage.save_connections(&[kept.clone(), removed.clone()]).await.unwrap(); @@ -10864,7 +10872,7 @@ mod tests { #[tokio::test] async fn desktop_settings_fall_back_to_legacy_background_preference() { let path = temp_db_path("desktop-settings-legacy-background"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut settings = serde_json::Map::new(); settings.insert("run_in_background".to_string(), serde_json::Value::Bool(false)); storage.save_app_settings_json(&settings).await.unwrap(); @@ -10886,7 +10894,7 @@ mod tests { #[tokio::test] async fn schema_cache_prunes_expired_rows_on_write_without_maintaining_during_reads() { let path = temp_db_path("schema-cache-ttl-prune"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .save_schema_cache( "object-ddl:v1:conn-a:db:public:old::TABLE:", @@ -10938,7 +10946,7 @@ mod tests { #[tokio::test] async fn schema_cache_budget_covers_multiple_connections_objects_and_facets() { let path = temp_db_path("schema-cache-capacity"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let policy = super::SchemaCachePolicy { max_total_bytes: 1_100, max_connection_bytes: 700, @@ -10980,7 +10988,7 @@ mod tests { #[tokio::test] async fn schema_cache_lru_keeps_recently_accessed_entries() { let path = temp_db_path("schema-cache-lru"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let policy = super::SchemaCachePolicy { max_total_bytes: i64::MAX, max_connection_bytes: i64::MAX, @@ -11018,7 +11026,7 @@ mod tests { const ENTRY_COUNT: usize = 50_000; const SAMPLE_COUNT: usize = 40; let path = temp_db_path("schema-cache-50k-read-performance"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .with_conn(|conn| { let transaction = conn @@ -11076,7 +11084,7 @@ mod tests { #[tokio::test] async fn desktop_settings_preserve_existing_password_hash() { let path = temp_db_path("desktop-settings-preserve-password"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_password_hash("hash-1").await.unwrap(); storage @@ -11126,7 +11134,7 @@ mod tests { #[tokio::test] async fn desktop_settings_roundtrip_custom_ai_skill_root() { let path = temp_db_path("desktop-settings-custom-ai-skill-root"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .save_desktop_settings(&DesktopSettings { @@ -11166,7 +11174,7 @@ mod tests { #[tokio::test] async fn desktop_settings_save_removes_legacy_background_preference() { let path = temp_db_path("desktop-settings-remove-legacy-background"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut settings = serde_json::Map::new(); settings.insert("run_in_background".to_string(), serde_json::Value::Bool(false)); storage.save_app_settings_json(&settings).await.unwrap(); @@ -11193,7 +11201,7 @@ mod tests { #[tokio::test] async fn desktop_settings_persist_sidebar_table_page_size() { let path = temp_db_path("desktop-settings-sidebar-page-size"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .save_desktop_settings(&DesktopSettings { sidebar_table_page_size: 1234, ..DesktopSettings::default() }) @@ -11206,7 +11214,7 @@ mod tests { #[tokio::test] async fn desktop_settings_persist_duckdb_worker_max_processes() { let path = temp_db_path("desktop-settings-duckdb-worker-max-processes"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .save_desktop_settings(&DesktopSettings { duckdb_worker_max_processes: 8, ..DesktopSettings::default() }) @@ -11219,7 +11227,7 @@ mod tests { #[tokio::test] async fn max_agent_turns_defaults_and_persists_clamped() { let path = temp_db_path("max-agent-turns"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(storage.load_max_agent_turns().await.unwrap(), crate::agent_loop::DEFAULT_MAX_AGENT_TURNS); @@ -11236,7 +11244,7 @@ mod tests { #[tokio::test] async fn max_retries_defaults_and_persists_clamped() { let path = temp_db_path("max-retries"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(storage.load_max_retries().await.unwrap(), crate::ai::DEFAULT_MAX_RETRIES); @@ -11254,7 +11262,7 @@ mod tests { #[tokio::test] async fn plugin_ai_tools_and_data_grants_persist_and_survive_legacy_settings_saves() { let path = temp_db_path("plugin-permissions"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert!(storage.load_ai_plugin_tool_plugin_ids().await.unwrap().is_empty()); assert!(storage.load_plugin_data_grants("io.dbx.chart").await.unwrap().is_empty()); @@ -11288,7 +11296,7 @@ mod tests { #[tokio::test] async fn max_retries_survives_stale_app_settings_save() { let path = temp_db_path("max-retries-stale-save"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let stale_settings = storage.load_app_settings_json().await.unwrap(); storage.save_max_retries(7).await.unwrap(); @@ -11300,7 +11308,7 @@ mod tests { #[tokio::test] async fn sql_file_upload_max_mb_defaults_and_persists_clamped() { let path = temp_db_path("sql-file-upload-max-mb"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!( storage.load_sql_file_upload_max_mb().await.unwrap(), @@ -11321,7 +11329,7 @@ mod tests { #[tokio::test] async fn sql_file_upload_max_mb_survives_stale_app_settings_save() { let path = temp_db_path("sql-file-upload-max-mb-stale-save"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let stale_settings = storage.load_app_settings_json().await.unwrap(); storage.save_sql_file_upload_max_mb(256).await.unwrap(); @@ -11333,7 +11341,7 @@ mod tests { #[tokio::test] async fn password_hash_preserves_existing_desktop_settings() { let path = temp_db_path("password-preserve-desktop-settings"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .save_desktop_settings(&DesktopSettings { @@ -11359,7 +11367,7 @@ mod tests { #[tokio::test] async fn pinned_tree_node_ids_default_to_empty() { let path = temp_db_path("pinned-tree-default"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(storage.load_pinned_tree_node_ids().await.unwrap(), Vec::::new()); } @@ -11367,7 +11375,7 @@ mod tests { #[tokio::test] async fn pinned_tree_node_ids_roundtrip_and_preserve_password_hash() { let path = temp_db_path("pinned-tree-roundtrip"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_password_hash("hash-3").await.unwrap(); storage.save_pinned_tree_node_ids(&["conn-1".to_string(), "conn-1:db:main".to_string()]).await.unwrap(); @@ -11382,7 +11390,7 @@ mod tests { #[tokio::test] async fn app_state_roundtrips_without_polluting_app_settings() { let path = temp_db_path("app-state-roundtrip"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_password_hash("hash-4").await.unwrap(); storage @@ -11456,7 +11464,7 @@ mod tests { #[tokio::test] async fn ai_chat_selection_roundtrips_in_local_app_state() { let path = temp_db_path("ai-chat-selection"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let selection = AiChatSelectionState { version: 1, active: Some(AiActiveModelSelection { config_id: "config-1".to_string(), model_id: "model-1".to_string() }), @@ -11483,7 +11491,7 @@ mod tests { #[tokio::test] async fn ai_chat_selection_loads_legacy_payload_without_template_defaults() { let path = temp_db_path("ai-chat-selection-legacy"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let legacy = serde_json::json!({ "version": 1, "active": { "configId": "config-1", "modelId": "model-1" }, @@ -11514,7 +11522,7 @@ mod tests { #[tokio::test] async fn tab_runtime_cache_roundtrips_binary_payloads() { let path = temp_db_path("tab-runtime-cache"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage .save_tab_runtime_cache("tab:1:result", vec![1, 2, 3, 4], 10, 3, Some("connection-1".to_string())) @@ -11538,7 +11546,7 @@ mod tests { #[tokio::test] async fn tab_runtime_cache_pruning_retains_live_entries_and_enforces_byte_budget() { let path = temp_db_path("tab-runtime-cache-prune"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); for (key, size) in [("live", 6usize), ("old", 5), ("new", 4)] { storage.save_tab_runtime_cache(key, vec![1; size], 1, 1, Some("connection-1".to_string())).await.unwrap(); } @@ -11564,7 +11572,7 @@ mod tests { #[tokio::test] async fn tab_runtime_cache_pruning_respects_orphan_grace_period() { let path = temp_db_path("tab-runtime-cache-orphan-grace"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); storage.save_tab_runtime_cache("fresh", vec![1], 1, 1, None).await.unwrap(); storage.save_tab_runtime_cache("crash-leftover", vec![2], 1, 1, None).await.unwrap(); storage @@ -11599,7 +11607,7 @@ mod tests { .unwrap(); } - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let entry = storage.load_tab_runtime_cache("legacy").await.unwrap().unwrap(); assert_eq!(entry.payload, vec![1, 2]); @@ -11636,7 +11644,7 @@ mod tests { .unwrap(); } - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let loaded = storage.load_saved_sql_file("legacy-sql").await.unwrap().unwrap(); assert_eq!(loaded.database, "sales"); @@ -11646,7 +11654,7 @@ mod tests { #[tokio::test] async fn saved_sql_summary_omits_sql_text_and_loads_file_on_demand() { let path = temp_db_path("saved-sql-summary"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let file = SavedSqlFile { id: "sql-1".to_string(), connection_id: "conn-1".to_string(), @@ -11687,7 +11695,7 @@ mod tests { #[tokio::test] async fn saved_sql_metadata_update_preserves_unloaded_sql_text() { let path = temp_db_path("saved-sql-preserve-unloaded-text"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut file = SavedSqlFile { id: "sql-1".to_string(), connection_id: "conn-1".to_string(), @@ -11752,7 +11760,7 @@ mod tests { .unwrap(); } - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let legacy = storage.load_saved_sql_file("legacy").await.unwrap().unwrap(); assert_eq!(legacy.catalog, None); @@ -11775,7 +11783,7 @@ mod tests { storage.save_saved_sql_file(&external).await.unwrap(); drop(storage); - let reopened = Storage::open(&path).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&path).await.unwrap(); assert_eq!(reopened.load_saved_sql_file("legacy").await.unwrap().unwrap().catalog, None); assert_eq!( reopened.load_saved_sql_file("external").await.unwrap().unwrap().catalog.as_deref(), @@ -11835,7 +11843,7 @@ mod tests { #[tokio::test] async fn ai_config_save_load_roundtrip() { let db = temp_db_path("ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("test-config", true); cfg.config.provider = AiProvider::ClaudeCodeCli; @@ -11868,7 +11876,7 @@ mod tests { #[tokio::test] async fn legacy_ai_and_tunnel_inline_secrets_are_migrated_on_reopen() { let db = temp_db_path("legacy-config-secret-migration"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut ai = make_ai_config("legacy", true); storage.save_ai_config_item(&ai).await.unwrap(); @@ -11893,7 +11901,7 @@ mod tests { .unwrap(); drop(storage); - let reopened = Storage::open(&db).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&db).await.unwrap(); let raw_ai = reopened .with_conn(|conn| { conn.query_row("SELECT config_json FROM ai_configs WHERE id = 'cfg-legacy'", [], |row| { @@ -11934,7 +11942,7 @@ mod tests { #[tokio::test] async fn opencode_cli_ai_config_roundtrip() { let db = temp_db_path("opencode-cli-ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("opencode-cli", true); cfg.config.provider = AiProvider::OpenCodeCli; @@ -11961,7 +11969,7 @@ mod tests { #[tokio::test] async fn cursor_cli_ai_config_roundtrip() { let db = temp_db_path("cursor-cli-ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("cursor-cli", true); cfg.config.provider = AiProvider::CursorCli; @@ -11988,7 +11996,7 @@ mod tests { #[tokio::test] async fn grok_cli_ai_config_roundtrip() { let db = temp_db_path("grok-cli-ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("grok-cli", true); cfg.config.provider = AiProvider::GrokCli; @@ -12012,7 +12020,7 @@ mod tests { #[tokio::test] async fn codebuddy_cli_ai_config_roundtrip() { let db = temp_db_path("codebuddy-cli-ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("codebuddy-cli", true); cfg.config.provider = AiProvider::CodeBuddyCli; @@ -12039,7 +12047,7 @@ mod tests { #[tokio::test] async fn anthropic_compatible_ai_config_roundtrip() { let db = temp_db_path("anthropic-compatible-ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("anthropic-compatible", true); cfg.config.provider = AiProvider::AnthropicCompatible; @@ -12065,7 +12073,7 @@ mod tests { #[tokio::test] async fn minimax_ai_config_roundtrip() { let db = temp_db_path("minimax-ai-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut cfg = make_ai_config("minimax", true); cfg.config.provider = AiProvider::MiniMax; @@ -12089,7 +12097,7 @@ mod tests { #[tokio::test] async fn ai_config_only_one_default() { let db = temp_db_path("ai-one-default"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let cfg1 = make_ai_config("config-a", true); let cfg2 = make_ai_config("config-b", true); @@ -12109,7 +12117,7 @@ mod tests { #[tokio::test] async fn ai_config_update_existing_to_default() { let db = temp_db_path("ai-update-default"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let cfg1 = make_ai_config("config-a", true); let cfg2 = make_ai_config("config-b", false); @@ -12132,7 +12140,7 @@ mod tests { #[tokio::test] async fn ai_config_duplicate_name_error() { let db = temp_db_path("ai-dup-name"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let cfg1 = make_ai_config("same-name", false); storage.save_ai_config_item(&cfg1).await.unwrap(); @@ -12149,7 +12157,7 @@ mod tests { #[tokio::test] async fn ai_config_set_default_switches() { let db = temp_db_path("ai-set-default"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let cfg1 = make_ai_config("first", true); let cfg2 = make_ai_config("second", false); @@ -12171,7 +12179,7 @@ mod tests { #[tokio::test] async fn ai_config_delete_default_no_cascade() { let db = temp_db_path("ai-delete-default"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let cfg1 = make_ai_config("default-one", true); let cfg2 = make_ai_config("other", false); @@ -12192,7 +12200,7 @@ mod tests { #[tokio::test] async fn ai_config_save_configs_batch() { let db = temp_db_path("ai-batch"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let configs = vec![make_ai_config("batch-a", true), make_ai_config("batch-b", false), make_ai_config("batch-c", false)]; @@ -12207,7 +12215,7 @@ mod tests { #[tokio::test] async fn ai_config_secret_fields_are_not_written_to_config_json() { let db = temp_db_path("ai-secret-at-rest"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let config = make_ai_config("secret-config", true); storage.save_ai_configs(std::slice::from_ref(&config)).await.unwrap(); let raw = storage @@ -12239,7 +12247,7 @@ mod tests { #[tokio::test] async fn ai_config_save_configs_clears_old_tables() { let db = temp_db_path("ai-clear-old"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); // Pre-populate old tables as if migration hasn't run yet storage.save_ai_config(&make_ai_config("legacy-active", false).config).await.unwrap(); @@ -12267,7 +12275,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_new_creates_timestamps() { let db = temp_db_path("pt-save-new"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let result = storage.save_prompt_template("t1", "Production Rules", "SELECT 1").await.unwrap(); @@ -12290,7 +12298,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_update_preserves_created_at() { let db = temp_db_path("pt-save-update"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let first = storage.save_prompt_template("t1", "Original Name", "Original content").await.unwrap(); // Ensure some time passes so updated_at changes @@ -12315,7 +12323,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_rejects_blank_name() { let db = temp_db_path("pt-name-blank"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let err = storage.save_prompt_template("t1", "", "content").await.unwrap_err(); assert!(err.contains("cannot be empty"), "expected 'cannot be empty', got: {err}"); @@ -12329,7 +12337,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_rejects_long_name() { let db = temp_db_path("pt-name-long"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let long_name = "a".repeat(51); let err = storage.save_prompt_template("t1", &long_name, "content").await.unwrap_err(); @@ -12341,7 +12349,7 @@ mod tests { #[tokio::test] async fn prompt_template_accepts_multi_byte_characters_within_char_limit() { let db = temp_db_path("pt-multibyte-name"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); // 25 Chinese characters = 75 bytes but only 25 chars — should be allowed under 50 char limit let name25 = "数".repeat(25); // 25 chars, 75 bytes @@ -12363,7 +12371,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_rejects_long_content() { let db = temp_db_path("pt-content-long"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let long_content = "a".repeat(8001); let err = storage.save_prompt_template("t1", "Valid Name", &long_content).await.unwrap_err(); @@ -12375,7 +12383,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_rejects_duplicate_name_case_insensitive() { let db = temp_db_path("pt-dup-name"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage.save_prompt_template("t1", "Production Rules", "content 1").await.unwrap(); @@ -12394,7 +12402,7 @@ mod tests { #[tokio::test] async fn prompt_template_save_rejects_duplicate_name_unicode_case_folding() { let db = temp_db_path("pt-dup-unicode"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); // SQLite LOWER() is ASCII-only (U+00C4 'Ä' → no change), but Rust // str::to_lowercase() does full Unicode case folding (Ä → ä). @@ -12405,7 +12413,7 @@ mod tests { // Reverse: lower-case first, upper-case second. let db = temp_db_path("pt-dup-unicode-2"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage.save_prompt_template("t1", "ä规则", "content-lower").await.unwrap(); let err = storage.save_prompt_template("t2", "Ä规则", "content-upper").await.unwrap_err(); assert!(err.contains("duplicate"), "expected 'duplicate', got: {err}"); @@ -12416,7 +12424,7 @@ mod tests { #[tokio::test] async fn prompt_template_load_order_is_stable() { let db = temp_db_path("pt-load-order"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); // Insert in reverse order of created_at by sleeping between inserts storage.save_prompt_template("a", "Template A", "a").await.unwrap(); @@ -12450,7 +12458,7 @@ mod tests { #[tokio::test] async fn prompt_template_delete_unknown_id_errors() { let db = temp_db_path("pt-delete-unknown"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let err = storage.delete_prompt_template("nonexistent").await.unwrap_err(); assert!(err.contains("not found"), "expected 'not found', got: {err}"); @@ -12461,7 +12469,7 @@ mod tests { #[tokio::test] async fn prompt_template_delete_existing_removes() { let db = temp_db_path("pt-delete-existing"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage.save_prompt_template("t1", "Template", "content").await.unwrap(); assert_eq!(storage.load_prompt_templates().await.unwrap().len(), 1); @@ -12477,7 +12485,7 @@ mod tests { #[tokio::test] async fn global_instructions_set_get_roundtrip() { let db = temp_db_path("gi-roundtrip"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let instructions = "Amounts are in cents. Always filter by date range."; storage.save_ai_global_custom_instructions(instructions).await.unwrap(); @@ -12491,7 +12499,7 @@ mod tests { #[tokio::test] async fn global_instructions_defaults_to_empty() { let db = temp_db_path("gi-default"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let loaded = storage.load_ai_global_custom_instructions().await.unwrap(); assert_eq!(loaded, ""); @@ -12502,7 +12510,7 @@ mod tests { #[tokio::test] async fn global_instructions_rejects_too_long() { let db = temp_db_path("gi-too-long"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let long = "a".repeat(8001); let err = storage.save_ai_global_custom_instructions(&long).await.unwrap_err(); @@ -12514,7 +12522,7 @@ mod tests { #[tokio::test] async fn global_instructions_empty_string_clears() { let db = temp_db_path("gi-clear"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage.save_ai_global_custom_instructions("Some instructions").await.unwrap(); assert_eq!(storage.load_ai_global_custom_instructions().await.unwrap(), "Some instructions"); @@ -12529,7 +12537,7 @@ mod tests { #[tokio::test] async fn global_instructions_whitespace_only_trims() { let db = temp_db_path("gi-whitespace"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage.save_ai_global_custom_instructions(" \n \t ").await.unwrap(); let loaded = storage.load_ai_global_custom_instructions().await.unwrap(); @@ -12541,11 +12549,11 @@ mod tests { #[tokio::test] async fn pending_snippet_cleanup_survives_restart_and_clears_only_when_matched() { let db = temp_db_path("snippet-cleanup-restart"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage.save_snippet_migration_state("github", "replacement-id", "legacy-id", "content-hash").await.unwrap(); drop(storage); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let state = storage.load_snippet_sync_state("github").await.unwrap(); assert_eq!(state.snippet_id.as_deref(), Some("replacement-id")); let pending = state.pending_cleanup.unwrap(); @@ -12565,7 +12573,7 @@ mod tests { #[tokio::test] async fn connection_secrets_are_encrypted_at_rest_and_legacy_rows_migrate() { let db = temp_db_path("secret-store-at-rest"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut url_config = plain_connection("url-params", "url-password"); url_config.url_params = Some("applicationName=dbx&PASSWORD=url-password&sslmode=require".to_string()); storage.save_connections(std::slice::from_ref(&url_config)).await.unwrap(); @@ -12641,7 +12649,7 @@ mod tests { // already lived in the secret store therefore lost it on upgrade. let id = "legacy-inline-url-params"; let db = temp_db_path("legacy-connection-config-keeps-password"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut config = plain_connection(id, "saved-password"); config.url_params = Some("authSource=dbx_test".to_string()); storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); @@ -12664,7 +12672,7 @@ mod tests { .unwrap(); drop(storage); - let reopened = Storage::open(&db).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&db).await.unwrap(); let raw = raw_connection_json(&reopened, id).await; assert!(!raw.contains("authSource=dbx_test")); assert_eq!(reopened.get_secret(id, "password").await.unwrap().as_deref(), Some("saved-password")); @@ -12679,7 +12687,7 @@ mod tests { async fn legacy_connection_config_migration_still_drops_password_when_not_saved() { let id = "legacy-inline-url-params-unsaved"; let db = temp_db_path("legacy-connection-config-drops-unsaved-password"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); let mut config = plain_connection(id, "saved-password"); config.url_params = Some("authSource=dbx_test".to_string()); storage.save_connections(std::slice::from_ref(&config)).await.unwrap(); @@ -12703,7 +12711,7 @@ mod tests { // "Don't save password" must keep winning: the restore path must not // resurrect a credential this connection is configured not to keep. - let reopened = Storage::open(&db).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&db).await.unwrap(); assert!(reopened.get_secret(id, "password").await.unwrap().is_none()); std::fs::remove_file(&db).ok(); } @@ -12711,7 +12719,7 @@ mod tests { #[tokio::test] async fn mqtt_password_is_encrypted_at_rest_and_hydrated_on_load() { let path = temp_db_path("mqtt-password-secret"); - let storage = Storage::open(&path).await.unwrap(); + let storage = crate::persistence::test_storage::open(&path).await.unwrap(); let mut config = plain_connection("mqtt-password", ""); config.db_type = DatabaseType::Mqtt; config.external_config = Some(serde_json::json!({ @@ -12733,7 +12741,7 @@ mod tests { #[tokio::test] async fn legacy_plaintext_secrets_are_migrated_during_reopen() { let db = temp_db_path("secret-store-startup-migration"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage .with_conn(|conn| { conn.execute( @@ -12747,7 +12755,7 @@ mod tests { .unwrap(); drop(storage); - let reopened = Storage::open(&db).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&db).await.unwrap(); let row = reopened .with_conn(|conn| { conn.query_row( @@ -12768,7 +12776,7 @@ mod tests { #[tokio::test] async fn legacy_sync_credentials_are_migrated_from_app_settings_on_reopen() { let db = temp_db_path("legacy-app-settings-secrets"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); storage .with_conn(|conn| { conn.execute( @@ -12787,7 +12795,7 @@ mod tests { .unwrap(); drop(storage); - let reopened = Storage::open(&db).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&db).await.unwrap(); let settings = reopened.load_app_settings_json().await.unwrap(); assert!(!settings.contains_key("local_device_secret")); assert!(!settings.contains_key("webdav_sync_secrets_passphrase")); @@ -12818,11 +12826,11 @@ mod tests { #[tokio::test] async fn legacy_inline_connection_secrets_are_migrated_during_reopen() { let db = temp_db_path("inline-secret-startup-migration"); - let storage = Storage::open(&db).await.unwrap(); + let storage = crate::persistence::test_storage::open(&db).await.unwrap(); insert_raw_connection(&storage, &plain_connection("legacy-inline", "old-inline-secret")).await; drop(storage); - let reopened = Storage::open(&db).await.unwrap(); + let reopened = crate::persistence::test_storage::open(&db).await.unwrap(); let raw = raw_connection_json(&reopened, "legacy-inline").await; assert!(!raw.contains("old-inline-secret")); assert_eq!(reopened.load_connections().await.unwrap()[0].password, "old-inline-secret"); @@ -12846,8 +12854,10 @@ mod tests { async fn sync_import_transaction_rolls_back_metadata_when_later_write_fails() { let directory = tempfile::tempdir().unwrap(); let db = directory.path().join("dbx.db"); - let storage = - Storage::open_unmigrated(&db).await.unwrap().with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); + let storage = crate::persistence::test_storage::open_unmigrated(&db) + .await + .unwrap() + .with_secret_key_policy(SecretKeyPolicy::ManagedDataDir); storage.save_connections(&[plain_connection("existing", "old-secret")]).await.unwrap(); let settings = storage.load_desktop_settings().await.unwrap(); let mut incoming = plain_connection("incoming", "new-secret"); diff --git a/crates/dbx-core/src/persistence/test_storage.rs b/crates/dbx-core/src/persistence/test_storage.rs new file mode 100644 index 0000000000..a0d2cc885c --- /dev/null +++ b/crates/dbx-core/src/persistence/test_storage.rs @@ -0,0 +1,42 @@ +use super::{ + secret_codec::{SecretCodec, SecretKeyPolicy}, + storage::Storage, +}; +use std::path::Path; + +pub async fn open(path: &Path) -> Result { + let directory = path.parent().ok_or("Test storage requires a data directory")?; + SecretCodec::resolve(SecretKeyPolicy::TestDataDir, directory, true)?; + Storage::open_with_secret_key_policy(path, SecretKeyPolicy::TestDataDir).await +} + +pub async fn open_unmigrated(path: &Path) -> Result { + Ok(Storage::open_unmigrated(path).await?.with_secret_key_policy(SecretKeyPolicy::TestDataDir)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::persistence::secret_codec::{managed_key_path, SecretCodec}; + + #[tokio::test] + async fn test_storage_keys_are_isolated_and_persist_across_reopens() { + let first_dir = tempfile::tempdir().unwrap(); + let second_dir = tempfile::tempdir().unwrap(); + let first_path = first_dir.path().join("dbx.db"); + let first = open(&first_path).await.unwrap(); + let first_key = SecretCodec::resolve(SecretKeyPolicy::TestDataDir, first_dir.path(), true).unwrap(); + let second_key = SecretCodec::resolve(SecretKeyPolicy::TestDataDir, second_dir.path(), true).unwrap(); + let encrypted = first_key.codec.encrypt("test", "credential", "fixture-value").unwrap(); + assert!(second_key.codec.decrypt("test", "credential", &encrypted).is_err()); + let first_bytes = std::fs::read(managed_key_path(first_dir.path())).unwrap(); + let second_bytes = std::fs::read(managed_key_path(second_dir.path())).unwrap(); + assert_ne!(first_bytes, second_bytes); + assert_eq!(first_key.source, second_key.source); + drop(first); + let _reopened = open(&first_path).await.unwrap(); + let reopened_key = SecretCodec::resolve(SecretKeyPolicy::TestDataDir, first_dir.path(), false).unwrap(); + assert_eq!(reopened_key.codec.decrypt("test", "credential", &encrypted).unwrap(), "fixture-value"); + assert_eq!(first_bytes, std::fs::read(managed_key_path(first_dir.path())).unwrap()); + } +} diff --git a/crates/dbx-core/src/query/mod.rs b/crates/dbx-core/src/query/mod.rs index 6e21af0349..0ec1c5efda 100644 --- a/crates/dbx-core/src/query/mod.rs +++ b/crates/dbx-core/src/query/mod.rs @@ -7026,7 +7026,9 @@ mod tests { fail_once: Option<&'static str>, ) -> (AppState, Arc>>, tempfile::TempDir) { let directory = tempfile::tempdir().unwrap(); - let state = AppState::new(Storage::open(&directory.path().join("storage.db")).await.unwrap()); + let state = AppState::new( + crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(), + ); let config = test_connection_config(DatabaseType::Postgres); state.configs.write().await.insert(config.id.clone(), config); let commands = Arc::new(Mutex::new(Vec::new())); @@ -7546,7 +7548,8 @@ mod tests { use std::sync::atomic::{AtomicBool, Ordering}; let dir = tempfile::tempdir().expect("temp dir"); - let storage = Storage::open(&dir.path().join("storage.db")).await.expect("open test storage"); + let storage = + crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.expect("open test storage"); let state = Arc::new(AppState::new(storage)); let connection_id = "sqlite-cancel-session"; let client_session_id = "query-tab-8414"; @@ -7764,7 +7767,6 @@ mod tests { InstalledPlugin, PluginCompatibility, PluginDriverManifest, PluginDriverSession, PluginManifest, PluginRuntimeEnv, }; - use crate::storage::Storage; #[cfg(unix)] async fn spawn_agent_batch_timeout_test_client() -> (AgentDriverClient, tempfile::NamedTempFile) { @@ -7855,7 +7857,7 @@ for line in sys.stdin: async fn query_and_transaction_paths_resolve_catalog_dialect_from_connection() { let dir = std::env::temp_dir().join(format!("dbx-catalog-dialect-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut doris = test_connection_config(DatabaseType::Doris); @@ -8117,7 +8119,7 @@ for line in sys.stdin: let (host, port) = address.rsplit_once(':').expect("DynamoDB endpoint must include a port"); let dir = std::env::temp_dir().join(format!("dbx-query-dynamodb-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut config = test_connection_config(DatabaseType::DynamoDb); config.host = host.to_string(); @@ -8205,7 +8207,7 @@ for line in sys.stdin: .unwrap(); runtime.increment_session_count(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert("conn-1".to_string(), test_connection_config(DatabaseType::SqlServer)); state @@ -8322,7 +8324,7 @@ for line in sys.stdin: .unwrap(); runtime.increment_session_count(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert("conn-1".to_string(), test_connection_config(DatabaseType::Dameng)); state @@ -8381,7 +8383,7 @@ for line in sys.stdin: async fn native_pre_dispatch_cancellation_stays_typed() { let dir = std::env::temp_dir().join(format!("dbx-query-native-cancel-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "sqlite-cancel"; let sqlite = db::sqlite::connect_path_create_if_missing(dir.join("query.db").to_str().unwrap()).await.unwrap(); @@ -8561,7 +8563,7 @@ for line in sys.stdin: #[tokio::test] async fn ddl_schema_cache_invalidates_persisted_object_snapshots() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "schema-cache"; let sqlite = @@ -8603,7 +8605,7 @@ for line in sys.stdin: ("BEGIN; ALTER TABLE users ADD COLUMN added INTEGER; COMMIT", false, false, true), ] { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let sqlite = db::sqlite::connect_path_create_if_missing(dir.path().join("query.db").to_str().unwrap()) .await @@ -8662,7 +8664,7 @@ for line in sys.stdin: async fn ddl_schema_cache_storage_failure_preserves_sql_outcome() { let dir = tempfile::tempdir().unwrap(); let storage_path = dir.path().join("storage.db"); - let state = AppState::new(Storage::open(&storage_path).await.unwrap()); + let state = AppState::new(crate::persistence::test_storage::open(&storage_path).await.unwrap()); let sqlite = db::sqlite::connect_path_create_if_missing(dir.path().join("query.db").to_str().unwrap()).await.unwrap(); state @@ -8691,7 +8693,7 @@ for line in sys.stdin: async fn assert_sqlite_batch_error_behavior(failure_first: bool, continue_on_error: bool) { let dir = std::env::temp_dir().join(format!("dbx-query-batch-error-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "sqlite-batch"; let sqlite = db::sqlite::connect_path_create_if_missing(dir.join("query.db").to_str().unwrap()).await.unwrap(); @@ -8742,7 +8744,7 @@ for line in sys.stdin: async fn transactional_sqlite_batch_rolls_back_when_a_later_statement_fails() { let dir = std::env::temp_dir().join(format!("dbx-query-transaction-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "sqlite-transaction"; let sqlite = db::sqlite::connect_path_create_if_missing(dir.join("query.db").to_str().unwrap()).await.unwrap(); @@ -8785,7 +8787,7 @@ for line in sys.stdin: async fn transactional_batch_rejects_an_unsupported_backend_before_execution() { let dir = std::env::temp_dir().join(format!("dbx-query-unsupported-transaction-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "message-queue-transaction"; state @@ -8877,7 +8879,7 @@ for line in sys.stdin: async fn connection_pool_is_sqlserver_agent_detects_agent_and_native_pools() { let dir = std::env::temp_dir().join(format!("dbx-query-sqlserver-agent-flag-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); // Non-SQL-Server connections never use the SQL Server agent splitter. @@ -8913,7 +8915,7 @@ for line in sys.stdin: // on failure for DDL that cannot be rolled back. let dir = std::env::temp_dir().join(format!("dbx-query-tx-predispatch-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "message-queue-tx-ddl"; state @@ -8987,7 +8989,7 @@ for line in sys.stdin: async fn gaussdb_on_error_stop_overrides_continue_on_error() { let dir = std::env::temp_dir().join(format!("dbx-query-gaussdb-on-error-stop-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "gaussdb-on-error-stop"; let sqlite = db::sqlite::connect_path_create_if_missing(dir.join("query.db").to_str().unwrap()).await.unwrap(); @@ -10233,7 +10235,7 @@ for line in sys.stdin: .await .expect("plugin should start"), ); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut config = test_connection_config(DatabaseType::Jdbc); config.id = "jdbc-conn".to_string(); @@ -11457,7 +11459,7 @@ for line in sys.stdin: let dir = std::env::temp_dir().join(format!("dbx-manual-txn-{}", uuid::Uuid::new_v4().simple())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut config = test_connection_config(db_type); config.id = "agent-conn".to_string(); diff --git a/crates/dbx-core/src/query/mongo_ops.rs b/crates/dbx-core/src/query/mongo_ops.rs index 80a0466ffc..f13f5320fe 100644 --- a/crates/dbx-core/src/query/mongo_ops.rs +++ b/crates/dbx-core/src/query/mongo_ops.rs @@ -1637,8 +1637,6 @@ mod tests { use crate::db::agent_driver::{AgentDriverClient, AgentLaunchSpec}; #[cfg(unix)] use crate::models::connection::ConnectionConfig; - #[cfg(unix)] - use crate::storage::Storage; #[cfg(unix)] async fn legacy_mongo_state( @@ -1796,7 +1794,7 @@ for line in sys.stdin: .await .unwrap(); client.try_optional_handshake("test").await.unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": "legacy", diff --git a/crates/dbx-core/src/query/object_cache.rs b/crates/dbx-core/src/query/object_cache.rs index 8ec76dca5b..bc05f5eaaa 100644 --- a/crates/dbx-core/src/query/object_cache.rs +++ b/crates/dbx-core/src/query/object_cache.rs @@ -114,7 +114,7 @@ mod tests { #[tokio::test] async fn ddl_schema_cache_encoding_and_connection_isolation() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let id = "conn:% 中文"; assert_eq!(metadata_cache_segment(id), "conn%3A%25%20%E4%B8%AD%E6%96%87"); let prefix = object_metadata_cache_prefix(id, "db% name"); diff --git a/crates/dbx-core/src/query/plugin_data.rs b/crates/dbx-core/src/query/plugin_data.rs index 5cdd54317c..287e507f14 100644 --- a/crates/dbx-core/src/query/plugin_data.rs +++ b/crates/dbx-core/src/query/plugin_data.rs @@ -402,7 +402,7 @@ mod tests { installer .install_bytes(&ui_plugin_package("sample.nodata", &[]), PluginInstallPolicy::LocalDevelopment) .unwrap(); - let storage = crate::storage::Storage::open(&root.path().join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&root.path().join("dbx.db")).await.unwrap(); let state = AppState::new_with_plugin_dir_and_app_version(storage, plugin_dir, "0.6.0"); let path = root.path().join("orders.db"); diff --git a/crates/dbx-core/src/query/plugin_plan.rs b/crates/dbx-core/src/query/plugin_plan.rs index fac177d117..2a625ac912 100644 --- a/crates/dbx-core/src/query/plugin_plan.rs +++ b/crates/dbx-core/src/query/plugin_plan.rs @@ -533,7 +533,6 @@ pub fn plugin_plan_timeout_secs(timeout_ms: Option, config: &ConnectionConf mod tests { use super::*; use crate::connection::PoolKind; - use crate::storage::Storage; fn config(db_type: DatabaseType, query_timeout_secs: u64) -> ConnectionConfig { serde_json::from_value(serde_json::json!({ @@ -560,7 +559,8 @@ mod tests { /// disconnected" situation the plan boundary has to refuse. async fn saved_connection_state(configs: &[ConnectionConfig]) -> (AppState, tempfile::TempDir) { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.expect("open storage"); + let storage = + crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.expect("open storage"); let state = AppState::new_with_plugin_dir(storage, dir.path().join("plugins")); { let mut stored = state.configs.write().await; diff --git a/crates/dbx-core/src/query/two_phase_commit.rs b/crates/dbx-core/src/query/two_phase_commit.rs index b82c26c2a0..fa3a0d4df4 100644 --- a/crates/dbx-core/src/query/two_phase_commit.rs +++ b/crates/dbx-core/src/query/two_phase_commit.rs @@ -529,9 +529,8 @@ mod tests { } async fn make_2pc() -> (TwoPhaseCommit, std::path::PathBuf) { - use crate::storage::Storage; let path = std::env::temp_dir().join(format!("test_2pc_{}.db", uuid::Uuid::new_v4())); - let storage = Arc::new(Storage::open(&path).await.unwrap()); + let storage = Arc::new(crate::persistence::test_storage::open(&path).await.unwrap()); let backend: Arc = Arc::new(crate::state_persistence::LocalBackend::new(storage)); (TwoPhaseCommit::new(backend), path) } diff --git a/crates/dbx-core/src/safety/write_unlock.rs b/crates/dbx-core/src/safety/write_unlock.rs index 6d592e35fe..8d9c3cfffb 100644 --- a/crates/dbx-core/src/safety/write_unlock.rs +++ b/crates/dbx-core/src/safety/write_unlock.rs @@ -97,7 +97,6 @@ mod tests { use crate::models::connection::ConnectionConfig; use crate::query::{check_read_only_for_connection, connection_readonly_name}; use crate::session_credentials::with_credential_owner; - use crate::storage::Storage; fn mysql_config(id: &str, name: &str, read_only: bool) -> ConnectionConfig { serde_json::from_value(serde_json::json!({ @@ -116,7 +115,8 @@ mod tests { async fn test_state(configs: &[ConnectionConfig]) -> (AppState, tempfile::TempDir) { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.expect("open storage"); + let storage = + crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.expect("open storage"); let state = AppState::new_with_plugin_dir(storage, dir.path().join("plugins")); { let mut stored = state.configs.write().await; diff --git a/crates/dbx-core/src/scheduled_backup/tests.rs b/crates/dbx-core/src/scheduled_backup/tests.rs index e8edf7f5ae..ee714ec97f 100644 --- a/crates/dbx-core/src/scheduled_backup/tests.rs +++ b/crates/dbx-core/src/scheduled_backup/tests.rs @@ -118,7 +118,7 @@ async fn recovery_preserves_queued_jobs_and_marks_orphaned_running_jobs_failed() } async fn service(dir: &std::path::Path, root: Option) -> BackupService { - let storage = crate::storage::Storage::open(&dir.join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("dbx.db")).await.unwrap(); BackupService::new(Arc::new(crate::connection::AppState::new(storage)), dir, root) } @@ -234,7 +234,7 @@ async fn worker_waits_for_security_migration_and_resumes_without_restarting() { let directory = tempfile::tempdir().unwrap(); let legacy_path = directory.path().join("connections.json"); std::fs::write(&legacy_path, "[]").unwrap(); - let storage = crate::storage::Storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); let service = BackupService::new(Arc::new(crate::connection::AppState::new(storage)), directory.path(), None); let queued = service.store.enqueue(request(schedule(directory.path()).config)).await.unwrap(); assert!(!service.state.storage.inspect_data_migration().await.unwrap().is_ready()); diff --git a/crates/dbx-core/src/schema/mod.rs b/crates/dbx-core/src/schema/mod.rs index dbdf77797b..ba5df52e0a 100644 --- a/crates/dbx-core/src/schema/mod.rs +++ b/crates/dbx-core/src/schema/mod.rs @@ -3164,7 +3164,6 @@ mod tests { use crate::plugins::{ InstalledPlugin, PluginDriverManifest, PluginDriverSession, PluginManifest, PluginRuntimeEnv, }; - use crate::storage::Storage; use std::collections::HashMap; use std::time::Duration; @@ -3295,7 +3294,7 @@ mod tests { async fn turso_test_state(base_url: &str) -> (AppState, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-turso-schema-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut config = test_connection_config(DatabaseType::Turso); config.database = Some("main".to_string()); @@ -3623,7 +3622,7 @@ done let session = std::sync::Arc::new( PluginDriverSession::start_for_test(plugin, "jdbc".to_string(), PluginRuntimeEnv::default()).await.unwrap(), ); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut config = test_connection_config(DatabaseType::Jdbc); config.id = "mysql-wrapper".to_string(); @@ -3785,7 +3784,7 @@ done let session = std::sync::Arc::new( PluginDriverSession::start_for_test(plugin, "jdbc".to_string(), PluginRuntimeEnv::default()).await.unwrap(), ); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let mut config = test_connection_config(DatabaseType::Jdbc); config.id = "oracle-synonym".to_string(); @@ -3933,7 +3932,7 @@ done let session = std::sync::Arc::new( PluginDriverSession::start_for_test(plugin, "jdbc".into(), PluginRuntimeEnv::default()).await.unwrap(), ); - let state = AppState::new(Storage::open(&dir.join("storage.db")).await.unwrap()); + let state = AppState::new(crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap()); let mut config = test_connection_config(DatabaseType::Jdbc); config.id = "oracle-ddl".into(); config.database = Some("demo".into()); @@ -4528,7 +4527,7 @@ done async fn metadata_pool_races_retry_once_and_saturation_returns_busy() { let dir = std::env::temp_dir().join(format!("dbx-schema-metadata-pool-race-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); state.configs.write().await.insert("conn".to_string(), test_connection_config(DatabaseType::Mysql)); @@ -4581,7 +4580,7 @@ done async fn metadata_pool_snapshot_releases_global_connections_lock() { let dir = std::env::temp_dir().join(format!("dbx-schema-metadata-snapshot-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let pool = crate::db::sqlite::connect_path(":memory:").await.unwrap(); state @@ -4623,7 +4622,7 @@ done async fn metadata_fail_stop_detaches_base_pool_without_client_session() { let dir = std::env::temp_dir().join(format!("dbx-schema-metadata-fail-stop-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let mut config = test_connection_config(DatabaseType::Dameng); config.id = "conn".to_string(); @@ -4647,7 +4646,7 @@ done async fn metadata_timeout_detaches_pool_without_replaying_operation() { let dir = std::env::temp_dir().join(format!("dbx-schema-metadata-timeout-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let mut config = test_connection_config(DatabaseType::Dameng); config.id = "conn".to_string(); @@ -4676,7 +4675,7 @@ done async fn metadata_second_quarantine_detaches_replacement_pool() { let dir = std::env::temp_dir().join(format!("dbx-schema-metadata-quarantine-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let mut config = test_connection_config(DatabaseType::Sqlite); config.id = "conn".to_string(); @@ -4758,7 +4757,7 @@ for line in sys.stdin: runtime.increment_session_count(); let client = crate::db::agent_driver::AgentDriverClient::shared_session(runtime.clone(), "metadata-session".to_string()); - let storage = crate::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = crate::connection::AppState::new(storage); let mut config = test_connection_config(DatabaseType::Dameng); config.id = "conn".to_string(); @@ -5988,7 +5987,7 @@ done let session = std::sync::Arc::new( PluginDriverSession::start_for_test(plugin, "jdbc".to_string(), PluginRuntimeEnv::default()).await.unwrap(), ); - let state = AppState::new(Storage::open(&dir.join("storage.db")).await.unwrap()); + let state = AppState::new(crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap()); let mut config = test_connection_config(DatabaseType::Jdbc); config.id = "jdbc-oracle-columns".to_string(); config.database = Some("demo".to_string()); @@ -6094,7 +6093,7 @@ done let session = std::sync::Arc::new( PluginDriverSession::start_for_test(plugin, "jdbc".to_string(), PluginRuntimeEnv::default()).await.unwrap(), ); - let state = AppState::new(Storage::open(&dir.join("storage.db")).await.unwrap()); + let state = AppState::new(crate::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap()); let mut config = test_connection_config(DatabaseType::Jdbc); config.id = "jdbc-mysql-columns".to_string(); config.database = Some("demo".to_string()); diff --git a/crates/dbx-core/src/schema/plugin_metadata.rs b/crates/dbx-core/src/schema/plugin_metadata.rs index 05d2e5d920..d95fe9d032 100644 --- a/crates/dbx-core/src/schema/plugin_metadata.rs +++ b/crates/dbx-core/src/schema/plugin_metadata.rs @@ -193,7 +193,6 @@ fn field_capability( #[cfg(test)] mod tests { use super::*; - use crate::storage::Storage; fn config() -> crate::models::connection::ConnectionConfig { serde_json::from_value(serde_json::json!({ @@ -269,7 +268,7 @@ mod tests { #[tokio::test] async fn saved_but_disconnected_context_is_rejected_without_creating_a_pool() { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.path().join("plugins")); let config = config(); state.configs.write().await.insert(config.id.clone(), config); diff --git a/crates/dbx-core/src/schema/plugin_metadata_tests.rs b/crates/dbx-core/src/schema/plugin_metadata_tests.rs index 169fcaa64c..e5c1d5571d 100644 --- a/crates/dbx-core/src/schema/plugin_metadata_tests.rs +++ b/crates/dbx-core/src/schema/plugin_metadata_tests.rs @@ -5,14 +5,13 @@ use super::{ use crate::connection::{AppState, PoolKind, METADATA_POOL_ACQUIRE_TIMEOUT}; use crate::db::agent_driver::{AgentDriverClient, AgentLaunchSpec, AgentRuntimeClient}; use crate::models::connection::{ConnectionConfig, DatabaseType}; -use crate::storage::Storage; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; async fn state(db_type: DatabaseType) -> (tempfile::TempDir, AppState) { let dir = tempfile::tempdir().unwrap(); - let storage = Storage::open(&dir.path().join("storage.db")).await.unwrap(); + let storage = crate::persistence::test_storage::open(&dir.path().join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_and_agent_dir_and_app_version( storage, dir.path().join("plugins"), diff --git a/crates/dbx-core/tests/database_export_partition_ddl.rs b/crates/dbx-core/tests/database_export_partition_ddl.rs index 5e613f7c54..70d1685012 100644 --- a/crates/dbx-core/tests/database_export_partition_ddl.rs +++ b/crates/dbx-core/tests/database_export_partition_ddl.rs @@ -17,7 +17,6 @@ use std::sync::Arc; use dbx_core::connection::AppState; use dbx_core::database_export::{export_database_sql_core, DatabaseExportRequest}; -use dbx_core::storage::Storage; use support::{ postgres_test_config, psql, psql_allow_failure, start_docker_postgres, start_docker_postgres_with_server_args, DockerPostgres, @@ -76,7 +75,7 @@ async fn run_database_export_of_partition_tree_has_no_duplicates_and_replays() { let dir = std::env::temp_dir().join(format!("dbx-export-partition-ddl-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let connection_id = "export-partition-ddl-conn"; @@ -237,7 +236,7 @@ async fn partition_tree_ddl_query_count_does_not_scale_with_partition_count() { let dir = std::env::temp_dir().join(format!("dbx-partition-query-count-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let connection_id = "partition-query-count-conn"; state.configs.write().await.insert(connection_id.to_string(), postgres_test_config(connection_id, container.port)); @@ -305,7 +304,7 @@ async fn display_ddl_error_distinguishes_wrong_relkind_from_missing_relation() { let dir = std::env::temp_dir().join(format!("dbx-partition-relkind-error-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let connection_id = "partition-relkind-error-conn"; state.configs.write().await.insert(connection_id.to_string(), postgres_test_config(connection_id, container.port)); diff --git a/crates/dbx-core/tests/database_export_prefetch.rs b/crates/dbx-core/tests/database_export_prefetch.rs index d6a1e34697..f470637779 100644 --- a/crates/dbx-core/tests/database_export_prefetch.rs +++ b/crates/dbx-core/tests/database_export_prefetch.rs @@ -7,7 +7,6 @@ use std::process::Command; use dbx_core::connection::AppState; use dbx_core::database_export::{export_database_sql_core, DatabaseExportRequest}; -use dbx_core::storage::Storage; use std::sync::Arc; use support::{postgres_test_config, psql, start_docker_postgres}; @@ -54,7 +53,7 @@ async fn run_database_export_writes_structure_and_data_for_all_tables() { let dir = std::env::temp_dir().join(format!("dbx-export-prefetch-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let connection_id = "export-prefetch-conn"; diff --git a/crates/dbx-core/tests/dump_docs_fixture.rs b/crates/dbx-core/tests/dump_docs_fixture.rs index bdbb874788..3ec689c11e 100644 --- a/crates/dbx-core/tests/dump_docs_fixture.rs +++ b/crates/dbx-core/tests/dump_docs_fixture.rs @@ -10,7 +10,6 @@ use dbx_core::docs::annotations::{ AnnotationFile, ColumnAnnotation, GroupAnnotation, ProjectAnnotation, TableAnnotation, }; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; -use dbx_core::storage::Storage; use std::collections::BTreeMap; fn live_postgres_config( @@ -140,7 +139,7 @@ async fn dump_keycloak_fixture() { let dir = std::env::temp_dir().join(format!("dbx-dump-docs-fixture-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config.clone()); diff --git a/crates/dbx-core/tests/live_clickhouse_query_result_export.rs b/crates/dbx-core/tests/live_clickhouse_query_result_export.rs index 9f9a757cc5..ecc9800d18 100644 --- a/crates/dbx-core/tests/live_clickhouse_query_result_export.rs +++ b/crates/dbx-core/tests/live_clickhouse_query_result_export.rs @@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use dbx_core::connection::AppState; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query_result_export::{export_query_result_core, ExportStatus, QueryResultExportRequest}; -use dbx_core::storage::Storage; use dbx_core::table_import::parse_xlsx_file; fn live_clickhouse_config( @@ -66,7 +65,7 @@ async fn live_clickhouse_query_result_export_xlsx_streams_random_order_query_onc let config = live_clickhouse_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-clickhouse-query-export-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); diff --git a/crates/dbx-core/tests/live_doris.rs b/crates/dbx-core/tests/live_doris.rs index 333ae59b15..31a32dfa16 100644 --- a/crates/dbx-core/tests/live_doris.rs +++ b/crates/dbx-core/tests/live_doris.rs @@ -11,7 +11,6 @@ use std::time::Duration; use dbx_core::connection::{connect_bare_metadata_pool, AppState}; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::schema::get_columns_core; -use dbx_core::storage::Storage; use mysql_async::prelude::Queryable; fn live_doris_config(id: &str) -> ConnectionConfig { @@ -42,7 +41,7 @@ fn live_doris_config(id: &str) -> ConnectionConfig { async fn doris_qualified_table_metadata_resolves_schema_database_with_comments() { let config = live_doris_config("doris-6590-live"); let db_path = std::env::temp_dir().join(format!("dbx-doris-6590-{}.db", uuid::Uuid::new_v4().simple())); - let storage = Storage::open(&db_path).await.expect("open temp storage"); + let storage = dbx_core::persistence::test_storage::open(&db_path).await.expect("open temp storage"); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config.clone()); diff --git a/crates/dbx-core/tests/live_manual_snapshot_rotation.rs b/crates/dbx-core/tests/live_manual_snapshot_rotation.rs index a9f61206ed..919f0affda 100644 --- a/crates/dbx-core/tests/live_manual_snapshot_rotation.rs +++ b/crates/dbx-core/tests/live_manual_snapshot_rotation.rs @@ -5,7 +5,6 @@ use dbx_core::query::{ execute_in_manual_transaction_with_options, execute_sql_statement, rollback_manual_transaction, ManualTransactionExecutionOptions, }; -use dbx_core::storage::Storage; use std::sync::Arc; fn live_config(prefix: &str, database_type: DatabaseType, default_port: u16) -> ConnectionConfig { @@ -29,7 +28,9 @@ fn live_config(prefix: &str, database_type: DatabaseType, default_port: u16) -> async fn setup(config: ConnectionConfig) -> (Arc, std::path::PathBuf, String) { let storage_path = std::env::temp_dir().join(format!("dbx-snapshot-rotation-{}.db", uuid::Uuid::new_v4().simple())); - let state = Arc::new(AppState::new(Storage::open(&storage_path).await.expect("temporary storage"))); + let state = Arc::new(AppState::new( + dbx_core::persistence::test_storage::open(&storage_path).await.expect("temporary storage"), + )); let table_name = format!("dbx_pr9434_{}", uuid::Uuid::new_v4().simple()); let database = config.database.clone().expect("database"); state.configs.write().await.insert(config.id.clone(), config.clone()); diff --git a/crates/dbx-core/tests/live_manual_transaction.rs b/crates/dbx-core/tests/live_manual_transaction.rs index e808ea3a0a..0b94850f84 100644 --- a/crates/dbx-core/tests/live_manual_transaction.rs +++ b/crates/dbx-core/tests/live_manual_transaction.rs @@ -8,7 +8,6 @@ use dbx_core::query::{ begin_manual_transaction, commit_manual_transaction, execute_in_manual_transaction, execute_sql_statement, rollback_manual_transaction, stream_rows_in_manual_transaction, }; -use dbx_core::storage::Storage; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -46,7 +45,7 @@ fn live_config(prefix: &str, db_type: DatabaseType, default_port: u16) -> Connec async fn app_state_with_config(config: ConnectionConfig) -> (Arc, std::path::PathBuf) { let db_path = std::env::temp_dir().join(format!("dbx-live-manual-txn-{}.db", uuid::Uuid::new_v4().simple())); - let storage = Storage::open(&db_path).await.expect("open temp storage"); + let storage = dbx_core::persistence::test_storage::open(&db_path).await.expect("open temp storage"); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(config.id.clone(), config); (state, db_path) diff --git a/crates/dbx-core/tests/live_mongodb_agent_tools.rs b/crates/dbx-core/tests/live_mongodb_agent_tools.rs index 587cf9959f..d71962d078 100644 --- a/crates/dbx-core/tests/live_mongodb_agent_tools.rs +++ b/crates/dbx-core/tests/live_mongodb_agent_tools.rs @@ -8,7 +8,6 @@ use dbx_core::agent_tools::{execute_tool, AgentSqlPermissions}; use dbx_core::connection::{AppState, PoolKind}; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::mongo_ops::mongo_create_index_core; -use dbx_core::storage::Storage; use mongodb::bson::{doc, Bson}; use mongodb::event::{command::CommandEvent, EventHandler}; use mongodb::options::ClientOptions; @@ -76,7 +75,7 @@ async fn call_agent_tool_with_limit( #[ignore = "requires DBX_LIVE_MONGODB_* env vars pointing at a MongoDB database with a non-empty stores collection"] async fn mongodb_agent_find_one_returns_real_data_and_keeps_writes_blocked() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let config = live_mongodb_config("live-mongodb-agent"); let database = config.database.clone().expect("database"); @@ -96,7 +95,7 @@ async fn mongodb_agent_find_one_returns_real_data_and_keeps_writes_blocked() { #[ignore = "requires DBX_LIVE_MONGODB_* env vars pointing at a MongoDB database with a products collection containing at least 8 documents"] async fn mongodb_agent_enforces_limits_and_find_skips_total_count() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let config = live_mongodb_config("live-mongodb-agent-limits"); let database = config.database.clone().expect("database"); @@ -143,7 +142,7 @@ async fn mongodb_agent_enforces_limits_and_find_skips_total_count() { async fn stalled_mongodb_index_build_does_not_block_another_connection() { let uri = std::env::var("DBX_LIVE_MONGODB_42_URL").expect("DBX_LIVE_MONGODB_42_URL"); let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let primary_id = "live-mongodb-42-index"; let database = "dbx_issue_7720_primary"; diff --git a/crates/dbx-core/tests/live_mongodb_legacy_agent_commands.rs b/crates/dbx-core/tests/live_mongodb_legacy_agent_commands.rs index 4a74b0b627..93a777cff1 100644 --- a/crates/dbx-core/tests/live_mongodb_legacy_agent_commands.rs +++ b/crates/dbx-core/tests/live_mongodb_legacy_agent_commands.rs @@ -53,7 +53,8 @@ async fn find_and_modify_commands_run_over_the_legacy_agent() { let (host, port) = endpoint.split_once(':').expect("host:port"); let files = tempfile::tempdir().unwrap(); let database = format!("dbx_legacy_fam_{}", uuid::Uuid::new_v4().simple()); - let state = AppState::new(Storage::open(&files.path().join("storage.db")).await.unwrap()); + let state = + AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("storage.db")).await.unwrap()); let id = "legacy-find-and-modify-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": id, "name": "Legacy findAndModify test", "db_type": "mongodb", "host": host, "port": port.parse::().unwrap(), "username": "", "password": "", "database": database, "driver_profile": "mongodb-legacy" })).unwrap(); state.configs.write().await.insert(id.into(), config); @@ -163,7 +164,8 @@ async fn distinct_runs_over_the_legacy_agent() { let (host, port) = endpoint.split_once(':').expect("host:port"); let files = tempfile::tempdir().unwrap(); let database = format!("dbx_legacy_distinct_{}", uuid::Uuid::new_v4().simple()); - let state = AppState::new(Storage::open(&files.path().join("storage.db")).await.unwrap()); + let state = + AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("storage.db")).await.unwrap()); let id = "legacy-distinct-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": id, "name": "Legacy distinct test", "db_type": "mongodb", "host": host, "port": port.parse::().unwrap(), "username": "", "password": "", "database": database, "driver_profile": "mongodb-legacy" })).unwrap(); state.configs.write().await.insert(id.into(), config); @@ -212,7 +214,8 @@ async fn collection_stats_and_create_database_run_over_the_legacy_agent() { let (host, port) = endpoint.split_once(':').expect("host:port"); let files = tempfile::tempdir().unwrap(); let database = format!("dbx_legacy_admin_{}", uuid::Uuid::new_v4().simple()); - let state = AppState::new(Storage::open(&files.path().join("storage.db")).await.unwrap()); + let state = + AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("storage.db")).await.unwrap()); let id = "legacy-admin-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": id, "name": "Legacy admin test", "db_type": "mongodb", "host": host, "port": port.parse::().unwrap(), "username": "", "password": "", "database": database, "driver_profile": "mongodb-legacy" })).unwrap(); state.configs.write().await.insert(id.into(), config); @@ -298,7 +301,8 @@ async fn dump_and_export_resolve_the_per_database_pool() { let (host, port) = endpoint.split_once(':').expect("host:port"); let files = tempfile::tempdir().unwrap(); let database = format!("dbx_legacy_pool_{}", uuid::Uuid::new_v4().simple()); - let state = AppState::new(Storage::open(&files.path().join("storage.db")).await.unwrap()); + let state = + AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("storage.db")).await.unwrap()); let id = "legacy-pool-key-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": id, "name": "Legacy pool key test", "db_type": "mongodb", "host": host, "port": port.parse::().unwrap(), "username": "", "password": "", "database": database, "driver_profile": "mongodb-legacy" })).unwrap(); state.configs.write().await.insert(id.into(), config); diff --git a/crates/dbx-core/tests/live_mysql57.rs b/crates/dbx-core/tests/live_mysql57.rs index 61d1e8d16a..0dfe2eb591 100644 --- a/crates/dbx-core/tests/live_mysql57.rs +++ b/crates/dbx-core/tests/live_mysql57.rs @@ -13,7 +13,6 @@ use dbx_core::query::{ use dbx_core::query_result_export::{export_query_result_core, ExportStatus, QueryResultExportRequest}; use dbx_core::sql::{split_sql_statements_for_database, SqlFileRequest}; use dbx_core::sql_file_import::execute_sql_file_path; -use dbx_core::storage::Storage; use dbx_core::table_import::{ build_import_insert_batch_from_rows, parse_csv_bytes, parse_xlsx_file, TableImportColumnMapping, }; @@ -46,7 +45,7 @@ fn live_mysql_sql_file_config(id: &str) -> ConnectionConfig { async fn app_state_with_config(config: ConnectionConfig) -> (AppState, std::path::PathBuf) { let db_path = std::env::temp_dir().join(format!("dbx-live-sql-file-{}.db", uuid::Uuid::new_v4().simple())); - let storage = Storage::open(&db_path).await.expect("open temp storage"); + let storage = dbx_core::persistence::test_storage::open(&db_path).await.expect("open temp storage"); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); (state, db_path) @@ -436,7 +435,7 @@ async fn live_mysql_query_result_export_xlsx_streams_single_query_without_duplic let config = live_mysql_query_export_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-query-export-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); @@ -531,7 +530,7 @@ async fn live_mysql_csv_temporal_export_round_trip_preserves_dbx_force_text_valu let config = live_mysql_query_export_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-issue-8803-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); @@ -645,7 +644,7 @@ async fn live_mysql_xlsx_export_can_outlive_query_timeout_while_rows_keep_arrivi let config = live_mysql_query_export_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-query-export-timeout-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); diff --git a/crates/dbx-core/tests/live_mysql_autocommit_snapshot.rs b/crates/dbx-core/tests/live_mysql_autocommit_snapshot.rs index bc0c4c8916..875c188b5b 100644 --- a/crates/dbx-core/tests/live_mysql_autocommit_snapshot.rs +++ b/crates/dbx-core/tests/live_mysql_autocommit_snapshot.rs @@ -12,7 +12,6 @@ use dbx_core::query::{ execute_multi_core_with_options_for_client_typed, execute_sql_statement, execute_sql_statement_with_options, ExecuteMultiResult, QueryExecutionOptions, }; -use dbx_core::storage::Storage; use std::sync::Arc; fn live_config(prefix: &str) -> ConnectionConfig { @@ -36,7 +35,9 @@ fn live_config(prefix: &str) -> ConnectionConfig { async fn setup(config: &ConnectionConfig) -> (Arc, std::path::PathBuf, String) { let storage_path = std::env::temp_dir().join(format!("dbx-autocommit-snapshot-{}.db", uuid::Uuid::new_v4().simple())); - let state = Arc::new(AppState::new(Storage::open(&storage_path).await.expect("temporary storage"))); + let state = Arc::new(AppState::new( + dbx_core::persistence::test_storage::open(&storage_path).await.expect("temporary storage"), + )); let table_name = format!("dbx_issue_9479_{}", uuid::Uuid::new_v4().simple()); let database = config.database.clone().expect("database"); state.configs.write().await.insert(config.id.clone(), config.clone()); @@ -122,7 +123,9 @@ async fn run_in_tab_keeping_transactions( /// cases, seeded with `(1, 0)`. async fn setup_probe(config: &ConnectionConfig, tag: &str) -> (Arc, std::path::PathBuf, String) { let storage_path = std::env::temp_dir().join(format!("{tag}-{}.db", uuid::Uuid::new_v4().simple())); - let state = Arc::new(AppState::new(Storage::open(&storage_path).await.expect("temporary storage"))); + let state = Arc::new(AppState::new( + dbx_core::persistence::test_storage::open(&storage_path).await.expect("temporary storage"), + )); let table_name = format!("dbx_issue_9749_{}", uuid::Uuid::new_v4().simple()); let database = config.database.clone().expect("database"); state.configs.write().await.insert(config.id.clone(), config.clone()); diff --git a/crates/dbx-core/tests/live_mysql_database_export.rs b/crates/dbx-core/tests/live_mysql_database_export.rs index a34abd14c7..3e33fe7db8 100644 --- a/crates/dbx-core/tests/live_mysql_database_export.rs +++ b/crates/dbx-core/tests/live_mysql_database_export.rs @@ -4,7 +4,6 @@ use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::execute_sql_statement; use dbx_core::sql::SqlFileRequest; use dbx_core::sql_file_import::execute_sql_file_path; -use dbx_core::storage::Storage; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -51,7 +50,7 @@ async fn live_mysql_selected_table_restore_preserves_unselected_tables() { let database = format!("dbx_restore_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-selected-restore-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); execute_sql_statement(&state, &connection_id, "", &format!("CREATE DATABASE `{database}`"), None, None) @@ -121,7 +120,7 @@ async fn live_mysql_database_export_restores_dependent_views() { let database = format!("dbx_export_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); @@ -248,7 +247,7 @@ async fn run_live_mysql_database_export_handles_many_tables_including_empty_tabl let database = format!("dbx_export_many_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-many-tables-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); @@ -354,7 +353,7 @@ async fn live_mysql_database_export_creates_missing_destination_directory() { let database = format!("dbx_export_missing_dir_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-missing-dir-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); @@ -422,7 +421,7 @@ async fn live_mysql_database_export_refuses_to_recreate_a_destination_that_disap let database = format!("dbx_export_vanished_dir_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-vanished-dir-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); @@ -504,7 +503,7 @@ async fn live_mysql_database_export_refuses_a_destination_that_vanished_before_i let database = format!("dbx_export_precfg_vanished_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-preconfigured-vanished-dir-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); diff --git a/crates/dbx-core/tests/live_mysql_export_table_order.rs b/crates/dbx-core/tests/live_mysql_export_table_order.rs index 177b897185..bea0e2d725 100644 --- a/crates/dbx-core/tests/live_mysql_export_table_order.rs +++ b/crates/dbx-core/tests/live_mysql_export_table_order.rs @@ -2,7 +2,6 @@ use dbx_core::connection::AppState; use dbx_core::database_export::{export_database_sql_core, DatabaseExportRequest}; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::execute_sql_statement; -use dbx_core::storage::Storage; use std::sync::Arc; fn live_mysql_config(id: &str) -> ConnectionConfig { @@ -54,7 +53,7 @@ async fn live_mysql_database_export_table_order_is_not_alphabetical_when_fk_reor let database = format!("dbx_export_order_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-export-order-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id)); diff --git a/crates/dbx-core/tests/live_mysql_object_statistics.rs b/crates/dbx-core/tests/live_mysql_object_statistics.rs index 40ac1087dd..bf240373fa 100644 --- a/crates/dbx-core/tests/live_mysql_object_statistics.rs +++ b/crates/dbx-core/tests/live_mysql_object_statistics.rs @@ -8,7 +8,6 @@ use dbx_core::connection::AppState; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::execute_sql_statement; -use dbx_core::storage::Storage; fn live_mysql_config(connection_id: &str, database: &str) -> ConnectionConfig { let host = std::env::var("DBX_LIVE_MYSQL_HOST").expect("DBX_LIVE_MYSQL_HOST"); @@ -50,7 +49,7 @@ async fn live_mysql_object_statistics_refresh_after_writes() { let table = format!("dbx_stats_fresh_{}", &suffix[..12]); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-object-statistics-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(connection_id.clone(), live_mysql_config(&connection_id, &database)); diff --git a/crates/dbx-core/tests/live_mysql_transfer.rs b/crates/dbx-core/tests/live_mysql_transfer.rs index 664eba3d94..1707f73198 100644 --- a/crates/dbx-core/tests/live_mysql_transfer.rs +++ b/crates/dbx-core/tests/live_mysql_transfer.rs @@ -1,7 +1,6 @@ use dbx_core::connection::AppState; use dbx_core::db::mysql; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; -use dbx_core::storage::Storage; use dbx_core::transfer::{ drop_backup_tables, execute_on_pool, rename_tables_to_backup, sort_tables_by_fk_dependency_with_foreign_keys, transfer_table, TransferContent, TransferMode, TransferOwnershipPolicy, TransferRequest, TransferTableNameCase, @@ -174,7 +173,7 @@ async fn run_live_mysql_cross_version_transfer_completes_on_small_stack() { ); let dir = task_tmp.join(format!("small-stack-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(source_connection_id.clone(), source_config); state.configs.write().await.insert(target_connection_id.clone(), target_config); @@ -329,7 +328,7 @@ async fn live_mysql_transfer_keeps_columns_whose_comment_mentions_foreign_key() ); let dir = task_tmp.join(format!("live-mysql-fk-comment-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(source_connection_id.clone(), source_config); state.configs.write().await.insert(target_connection_id.clone(), target_config); @@ -486,7 +485,7 @@ async fn live_mysql_transfer_downgrades_unsupported_source_collations() { ); let dir = task_tmp.join(format!("live-mysql-collation-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(source_connection_id.clone(), source_config); state.configs.write().await.insert(target_connection_id.clone(), target_config); @@ -673,7 +672,7 @@ async fn live_mysql_transfer_preserves_spatial_values_and_modes() { let dir = std::env::temp_dir().join(format!("dbx-live-mysql-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -856,7 +855,7 @@ async fn live_mysql_transfer_structure_overwrite_rejects_incompatible_target_col let dir = std::env::temp_dir().join(format!("dbx-live-mysql-transfer-struct-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -1012,7 +1011,7 @@ async fn live_mysql_transfer_structure_only_rejects_incompatible_target_columns( let dir = std::env::temp_dir().join(format!("dbx-live-mysql-transfer-structonly-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -1111,7 +1110,7 @@ async fn live_mysql_transfer_drop_target_parent_child_foreign_key() { mysql::execute_query(&setup_pool, &setup, true).await.unwrap(); let dir = std::env::temp_dir().join(format!("dbx-live-mysql-parent-child-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -1304,7 +1303,7 @@ async fn live_mysql_transfer_drop_target_rebuilds_incompatible_structure() { let dir = std::env::temp_dir().join(format!("dbx-mysql-drop-rebuild-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -1493,7 +1492,7 @@ async fn live_mysql_transfer_drop_target_rejects_external_incoming_fk() { let dir = std::env::temp_dir().join(format!("dbx-mysql-ext-fk-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await.unwrap(); @@ -1583,7 +1582,7 @@ async fn live_mysql_transfer_drop_target_circular_foreign_keys() { let dir = std::env::temp_dir().join(format!("dbx-mysql-circular-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -1812,7 +1811,7 @@ async fn live_mysql_transfer_drop_target_retains_backup_on_failure() { let dir = std::env::temp_dir().join(format!("dbx-mysql-retain-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let source_pool_key = state.get_or_create_pool(&connection_id, Some(&source_database)).await.unwrap(); @@ -1989,7 +1988,7 @@ async fn live_mysql_keyset_pagination_copies_every_row() { ); let dir = task_tmp.join(format!("live-mysql-keyset-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(source_connection_id.clone(), source_config); state.configs.write().await.insert(target_connection_id.clone(), target_config); @@ -2109,7 +2108,7 @@ async fn live_mysql_progress_read_survives_total_duration_beyond_timeout() { ); let dir = task_tmp.join(format!("live-mysql-progress-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); // The read runs under the source's query timeout; keep it at 1s so the test only // passes when the transfer treats it as an inactivity budget, not a wall clock. @@ -2215,7 +2214,7 @@ async fn live_mysql_transfer_drop_target_inspects_dependent_views_without_the_us let dir = std::env::temp_dir().join(format!("dbx-mysql-legacy-dependency-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); let pool_key = state.get_or_create_pool(&connection_id, Some(&target_database)).await.unwrap(); diff --git a/crates/dbx-core/tests/live_oceanbase_sql_file_transaction.rs b/crates/dbx-core/tests/live_oceanbase_sql_file_transaction.rs index e881d82568..c08ca9b984 100644 --- a/crates/dbx-core/tests/live_oceanbase_sql_file_transaction.rs +++ b/crates/dbx-core/tests/live_oceanbase_sql_file_transaction.rs @@ -7,7 +7,6 @@ use dbx_core::query::{ begin_manual_transaction, commit_manual_transaction, execute_sql_statement, rollback_manual_transaction, }; use dbx_core::sql::{SqlFileRequest, SqlFileStatus}; -use dbx_core::storage::Storage; use std::time::{Duration, Instant}; use tokio_util::sync::CancellationToken; @@ -38,7 +37,7 @@ async fn verify_committed_value(state: &AppState, database: &str, table: &str, e #[ignore = "requires an installed OceanBase Oracle agent and writable DBX_LIVE_SQL_FILE_OB_* environment"] async fn live_oceanbase_sql_files_commit_rollback_failure_and_cancel() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("state.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("state.db")).await.unwrap(); let state = AppState::new_with_plugin_and_agent_dir_and_app_version( storage, directory.path().join("plugins"), diff --git a/crates/dbx-core/tests/live_postgres_all_schema_export.rs b/crates/dbx-core/tests/live_postgres_all_schema_export.rs index 422b9d6210..44a18c30d7 100644 --- a/crates/dbx-core/tests/live_postgres_all_schema_export.rs +++ b/crates/dbx-core/tests/live_postgres_all_schema_export.rs @@ -4,7 +4,6 @@ use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::execute_sql_statement; use dbx_core::sql::SqlFileRequest; use dbx_core::sql_file_import::execute_sql_file_path; -use dbx_core::storage::Storage; use std::sync::{Arc, Mutex}; use tokio_util::sync::CancellationToken; @@ -44,7 +43,8 @@ async fn live_postgres_all_schema_export_restores_one_sql_file() { let source_connection_id = format!("postgres-all-schema-source-{suffix}"); let target_connection_id = format!("postgres-all-schema-target-{suffix}"); let dir = tempfile::tempdir().expect("create export temp directory"); - let storage = Storage::open(&dir.path().join("storage.db")).await.expect("open temp storage"); + let storage = + dbx_core::persistence::test_storage::open(&dir.path().join("storage.db")).await.expect("open temp storage"); let state = Arc::new(AppState::new(storage)); state .configs diff --git a/crates/dbx-core/tests/live_postgres_docs_annotations.rs b/crates/dbx-core/tests/live_postgres_docs_annotations.rs index bcda63c8ad..56c4008aeb 100644 --- a/crates/dbx-core/tests/live_postgres_docs_annotations.rs +++ b/crates/dbx-core/tests/live_postgres_docs_annotations.rs @@ -7,7 +7,6 @@ use dbx_core::connection::AppState; use dbx_core::docs::{NoteSource, SnapshotWarning}; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; -use dbx_core::storage::Storage; fn live_postgres_config( id: &str, @@ -130,7 +129,7 @@ async fn annotations_reach_the_generated_dbml() { let dir = std::env::temp_dir().join(format!("dbx-live-postgres-docs-annotations-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config.clone()); diff --git a/crates/dbx-core/tests/live_postgres_docs_snapshot.rs b/crates/dbx-core/tests/live_postgres_docs_snapshot.rs index 229708c673..ed6c841a72 100644 --- a/crates/dbx-core/tests/live_postgres_docs_snapshot.rs +++ b/crates/dbx-core/tests/live_postgres_docs_snapshot.rs @@ -2,7 +2,6 @@ use dbx_core::connection::AppState; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; -use dbx_core::storage::Storage; fn live_postgres_config( id: &str, @@ -93,7 +92,7 @@ async fn collects_a_snapshot_and_serializes_valid_dbml() { let dir = std::env::temp_dir().join(format!("dbx-live-postgres-docs-snapshot-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config.clone()); diff --git a/crates/dbx-core/tests/live_postgres_error_position.rs b/crates/dbx-core/tests/live_postgres_error_position.rs index eb574c4f29..627018b43a 100644 --- a/crates/dbx-core/tests/live_postgres_error_position.rs +++ b/crates/dbx-core/tests/live_postgres_error_position.rs @@ -20,7 +20,6 @@ use dbx_core::db::postgres; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::{execute_multi_core_with_options_for_client_and_progress_typed, QueryExecutionOptions}; use dbx_core::sql_error_position::{encode_marker, take_message_position, SqlErrorPosition, SQL_ERROR_POSITION_MARKER}; -use dbx_core::storage::Storage; fn live_env() -> (String, u16, String, String, String) { let host = std::env::var("DBX_LIVE_POSTGRES_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); @@ -110,7 +109,7 @@ async fn live_pg_multi_core_reports_error_position_in_the_backend_envelope() { let dir = std::env::temp_dir().join(format!("dbx-live-pg-position-{}", uuid::Uuid::new_v4().simple())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-pg-position"; state.configs.write().await.insert( diff --git a/crates/dbx-core/tests/live_postgres_query_result_export.rs b/crates/dbx-core/tests/live_postgres_query_result_export.rs index 31cb3b0299..24f0440a6b 100644 --- a/crates/dbx-core/tests/live_postgres_query_result_export.rs +++ b/crates/dbx-core/tests/live_postgres_query_result_export.rs @@ -7,7 +7,6 @@ use dbx_core::db::postgres; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::execute_sql_statement; use dbx_core::query_result_export::{export_query_result_core, ExportStatus, QueryResultExportRequest}; -use dbx_core::storage::Storage; fn live_postgres_config( id: &str, @@ -117,7 +116,7 @@ async fn live_postgres_query_result_export_uses_single_streamed_query() { let dir = std::env::temp_dir().join(format!("dbx-live-postgres-query-export-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-postgres-query-export"; let config = live_postgres_config(connection_id, &host, port, &user, &password, &database); @@ -206,7 +205,7 @@ async fn live_postgres_query_result_xlsx_preserves_temporal_cell_types() { let dir = std::env::temp_dir().join(format!("dbx-live-postgres-xlsx-temporal-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-postgres-xlsx-temporal"; let config = live_postgres_config(connection_id, &host, port, &user, &password, &database); @@ -275,7 +274,7 @@ async fn live_postgres_numeric_xlsx_ignores_fractional_trailing_zeros() { let suffix = uuid::Uuid::new_v4().simple().to_string(); let dir = std::env::temp_dir().join(format!("dbx-live-postgres-xlsx-numeric-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-postgres-xlsx-numeric"; let config = live_postgres_config(connection_id, &host, port, &user, &password, &database); @@ -354,7 +353,7 @@ async fn live_postgres_truncated_batch_result_export_replays_safe_temp_setup() { let config = live_postgres_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-postgres-temp-export-{short_suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); @@ -447,7 +446,7 @@ async fn live_postgres_xlsx_export_can_outlive_query_timeout_while_rows_keep_arr let config = live_postgres_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-postgres-query-export-timeout-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); @@ -521,7 +520,7 @@ async fn live_postgres_stream_still_times_out_without_progress_and_recovers() { let config = live_postgres_config(&connection_id, &host, port, &user, &password, &database); let dir = std::env::temp_dir().join(format!("dbx-live-postgres-query-export-stall-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); state.configs.write().await.insert(config.id.clone(), config); diff --git a/crates/dbx-core/tests/live_postgres_transfer.rs b/crates/dbx-core/tests/live_postgres_transfer.rs index 5f60b87088..4cfa24168f 100644 --- a/crates/dbx-core/tests/live_postgres_transfer.rs +++ b/crates/dbx-core/tests/live_postgres_transfer.rs @@ -1,7 +1,6 @@ use dbx_core::connection::{AppState, PoolKind}; use dbx_core::db::postgres; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; -use dbx_core::storage::Storage; use dbx_core::transfer::{ drop_backup_tables, get_db_type, rename_tables_to_backup, transfer_postgres_schema_dependencies, transfer_postgres_schema_objects, transfer_table, TransferContent, TransferMode, TransferObjectKind, @@ -216,7 +215,7 @@ async fn live_postgres_transfer_upserts_generated_always_identity_values() { let dir = std::env::temp_dir().join(format!("dbx-live-always-transfer-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-always-source"; let target_connection_id = "live-always-target"; @@ -393,7 +392,7 @@ async fn live_postgres_structure_only_preserves_table_indexes() { let dir = std::env::temp_dir().join(format!("dbx-live-structure-only-transfer-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-structure-only-source"; let target_connection_id = "live-structure-only-target"; @@ -675,7 +674,7 @@ async fn live_postgres_transfer_preserves_data_and_schema_objects() { let dir = std::env::temp_dir().join(format!("dbx-live-transfer-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-source"; @@ -977,7 +976,7 @@ async fn live_postgres_transfer_skips_create_ddl_for_existing_target_table() { let dir = std::env::temp_dir().join(format!("dbx-live-existing-transfer-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-existing-source"; @@ -1103,7 +1102,7 @@ async fn live_postgres_transfer_creates_selected_sequence_before_referencing_tab let dir = std::env::temp_dir().join(format!("dbx-live-sequence-transfer-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-sequence-source"; let target_connection_id = "live-sequence-target"; @@ -1305,7 +1304,7 @@ async fn live_postgres_transfer_drop_target_rebuilds_structure_and_indexes() { let dir = std::env::temp_dir().join(format!("dbx-live-drop-rebuild-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), config); @@ -1496,7 +1495,9 @@ impl PostgresRebuildFixture { let source_pool_key = format!("{source_connection_id}:{source_database}"); let target_pool_key = format!("{target_connection_id}:{target_database}"); let storage_dir = tempfile::tempdir().unwrap(); - let state = Arc::new(AppState::new(Storage::open(&storage_dir.path().join("storage.db")).await.unwrap())); + let state = Arc::new(AppState::new( + dbx_core::persistence::test_storage::open(&storage_dir.path().join("storage.db")).await.unwrap(), + )); state .update_connection_pools(|connections| { connections.insert(source_pool_key.clone(), PoolKind::Postgres(source_pool.clone())); @@ -1816,7 +1817,7 @@ async fn live_postgres_keyset_pagination_copies_every_row() { let dir = std::env::temp_dir().join(format!("dbx-live-pg-keyset-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-pg-keyset-source"; let target_connection_id = "live-pg-keyset-target"; @@ -1928,7 +1929,7 @@ async fn live_postgres_progress_read_survives_total_duration_beyond_timeout() { let dir = std::env::temp_dir().join(format!("dbx-live-pg-progress-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-pg-progress-source"; let target_connection_id = "live-pg-progress-target"; @@ -2034,7 +2035,7 @@ async fn live_postgres_keyset_large_batch_copies_every_row() { let dir = std::env::temp_dir().join(format!("dbx-live-pg-largebatch-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let source_connection_id = "live-pg-largebatch-source"; let target_connection_id = "live-pg-largebatch-target"; diff --git a/crates/dbx-core/tests/live_redis_batch_expiry.rs b/crates/dbx-core/tests/live_redis_batch_expiry.rs index 2dc19193df..48a93c641e 100644 --- a/crates/dbx-core/tests/live_redis_batch_expiry.rs +++ b/crates/dbx-core/tests/live_redis_batch_expiry.rs @@ -7,7 +7,6 @@ use dbx_core::redis_ops::{ redis_delete_keys_in_db_core, redis_get_ttl_in_db_core, redis_set_keys_expire_at_in_db_core, redis_set_keys_ttl_in_db_core, redis_set_string_in_db_core, }; -use dbx_core::storage::Storage; /// Shared Redis fixtures used by the batch-expiration coverage below. /// @@ -20,7 +19,7 @@ async fn live_state(connection_id: &str) -> Arc { let username = std::env::var("DBX_LIVE_REDIS_USERNAME").unwrap_or_default(); let password = std::env::var("DBX_LIVE_REDIS_PASSWORD").unwrap_or_default(); let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let redis_config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": connection_id, diff --git a/crates/dbx-core/tests/live_redis_pool_registry.rs b/crates/dbx-core/tests/live_redis_pool_registry.rs index d83b10bc70..0b1c36bc22 100644 --- a/crates/dbx-core/tests/live_redis_pool_registry.rs +++ b/crates/dbx-core/tests/live_redis_pool_registry.rs @@ -4,7 +4,6 @@ use std::time::Duration; use dbx_core::connection::AppState; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::redis_ops::redis_execute_command_core; -use dbx_core::storage::Storage; #[tokio::test] #[ignore = "requires DBX_LIVE_REDIS_HOST and DBX_LIVE_REDIS_PORT"] @@ -12,7 +11,7 @@ async fn blocking_redis_command_does_not_block_another_connection() { let host = std::env::var("DBX_LIVE_REDIS_HOST").expect("DBX_LIVE_REDIS_HOST"); let port = std::env::var("DBX_LIVE_REDIS_PORT").expect("DBX_LIVE_REDIS_PORT").parse::().expect("Redis port"); let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let redis_config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": "live-redis-blocking", diff --git a/crates/dbx-core/tests/live_sqlserver_completion.rs b/crates/dbx-core/tests/live_sqlserver_completion.rs index fbcffa18cc..ac3abd1811 100644 --- a/crates/dbx-core/tests/live_sqlserver_completion.rs +++ b/crates/dbx-core/tests/live_sqlserver_completion.rs @@ -3,7 +3,6 @@ use dbx_core::models::connection::DatabaseType; use dbx_core::query_result_export::{export_query_result_core, ExportStatus, QueryResultExportRequest}; use dbx_core::sql::{SqlFileRequest, SqlFileStatus}; use dbx_core::sql_file_import::execute_sql_file_content; -use dbx_core::storage::Storage; use dbx_core::table_import::{ build_import_insert_batches, import_table_file_core, parse_delimited_file_with_options, TableImportColumnMapping, TableImportMode, TableImportParseOptions, TableImportRequest, TableImportSourceFormat, TableImportStatus, @@ -91,7 +90,8 @@ async fn live_sqlserver_import_state( ) -> (AppState, String, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-import-{suffix}")); std::fs::create_dir_all(&dir).expect("create live import directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("open live import storage"); + let storage = + dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.expect("open live import storage"); let state = AppState::new(storage); let config = live_sqlserver_config(connection_id, database); state.configs.write().await.insert(connection_id.to_string(), config); @@ -1169,7 +1169,7 @@ async fn live_sqlserver_query_result_export_streams_cte_query_to_csv() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-export-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-sqlserver-export"; let pool_key = format!("{connection_id}:{database}"); @@ -1262,7 +1262,7 @@ async fn live_sqlserver_sql_file_import_executes_go_batches() { let procedure = format!("dbx_sql_file_proc_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-file-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-sqlserver-file"; let mut config = live_sqlserver_config(connection_id, &database); @@ -1369,7 +1369,7 @@ async fn live_sqlserver_transfer_table_skips_rowversion_insert_column() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-rowversion-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let config = live_sqlserver_config("live-sqlserver-rowversion", &database); state.configs.write().await.insert(config.id.clone(), config); @@ -1565,7 +1565,7 @@ async fn live_sqlserver_cross_database_metadata_and_query() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-cross-database-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-sqlserver-cross-database"; let mut config = live_sqlserver_config(connection_id, &default_database); diff --git a/crates/dbx-core/tests/live_sqlserver_ddl_fk_display.rs b/crates/dbx-core/tests/live_sqlserver_ddl_fk_display.rs index d89a536224..6c82012b90 100644 --- a/crates/dbx-core/tests/live_sqlserver_ddl_fk_display.rs +++ b/crates/dbx-core/tests/live_sqlserver_ddl_fk_display.rs @@ -7,7 +7,6 @@ use dbx_core::connection::AppState; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::query::execute_sql_statement; -use dbx_core::storage::Storage; use std::sync::Arc; fn live_sqlserver_config(id: &str, database: &str) -> ConnectionConfig { @@ -44,7 +43,7 @@ async fn live_sqlserver_display_ddl_reads_foreign_key_actions() { let database = format!("dbx_ddl_fk_{suffix}"); let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-ddl-fk-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_sqlserver_config(&connection_id, "master")); diff --git a/crates/dbx-core/tests/live_sqlserver_query_result_export.rs b/crates/dbx-core/tests/live_sqlserver_query_result_export.rs index d8d511857b..5671718855 100644 --- a/crates/dbx-core/tests/live_sqlserver_query_result_export.rs +++ b/crates/dbx-core/tests/live_sqlserver_query_result_export.rs @@ -1,7 +1,6 @@ use dbx_core::connection::{AppState, PoolKind}; use dbx_core::models::connection::DatabaseType; use dbx_core::query_result_export::{export_query_result_core, ExportStatus, QueryResultExportRequest}; -use dbx_core::storage::Storage; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -89,7 +88,7 @@ async fn live_sqlserver_xlsx_export_can_outlive_query_timeout_while_rows_keep_ar let suffix = uuid::Uuid::new_v4().simple().to_string(); let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-xlsx-{suffix}")); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "live-sqlserver-xlsx-export"; let pool_key = format!("{connection_id}:{database}"); diff --git a/crates/dbx-core/tests/live_sqlserver_transfer.rs b/crates/dbx-core/tests/live_sqlserver_transfer.rs index c1e776af0a..3eee1ca370 100644 --- a/crates/dbx-core/tests/live_sqlserver_transfer.rs +++ b/crates/dbx-core/tests/live_sqlserver_transfer.rs @@ -1,6 +1,5 @@ use dbx_core::connection::AppState; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; -use dbx_core::storage::Storage; use dbx_core::transfer::{ drop_backup_tables, rename_tables_to_backup, transfer_table, TransferContent, TransferMode, TransferOwnershipPolicy, TransferRequest, TransferTableNameCase, @@ -155,7 +154,8 @@ async fn live_sqlserver_transfer_rebuild_releases_constraint_and_index_names() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-rebuild-{suffix}")); std::fs::create_dir_all(&dir).expect("create rebuild directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("open rebuild storage"); + let storage = + dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.expect("open rebuild storage"); let state = Arc::new(AppState::new(storage)); let config = live_sqlserver_config(&connection_id, &source_db); state.configs.write().await.insert(connection_id.clone(), config); @@ -318,7 +318,8 @@ async fn live_sqlserver_transfer_overwrite_handles_existing_identity_target() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-8690-{suffix}")); std::fs::create_dir_all(&dir).expect("create issue #8690 directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("open issue #8690 storage"); + let storage = + dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.expect("open issue #8690 storage"); let state = Arc::new(AppState::new(storage)); state.configs.write().await.insert(connection_id.clone(), live_sqlserver_config(&connection_id, &database)); let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await.expect("create SQL Server pool"); @@ -423,7 +424,8 @@ async fn live_sqlserver_keyset_pagination_copies_every_row() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-keyset-{suffix}")); std::fs::create_dir_all(&dir).expect("create keyset directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("open keyset storage"); + let storage = + dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.expect("open keyset storage"); let state = Arc::new(AppState::new(storage)); state .configs @@ -543,7 +545,8 @@ async fn live_sqlserver_progress_read_survives_total_duration_beyond_timeout() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-progress-{suffix}")); std::fs::create_dir_all(&dir).expect("create progress directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("open progress storage"); + let storage = + dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.expect("open progress storage"); let state = Arc::new(AppState::new(storage)); // The read runs under the source's query timeout; keep it at 1s so the test only // passes when the transfer treats it as an inactivity budget, not a wall clock. @@ -662,7 +665,7 @@ async fn live_sqlserver_keyset_uniqueidentifier_datetime2_composite_key() { let dir = std::env::temp_dir().join(format!("dbx-live-sqlserver-typed-{suffix}")); std::fs::create_dir_all(&dir).expect("create typed directory"); - let storage = Storage::open(&dir.join("storage.db")).await.expect("open typed storage"); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.expect("open typed storage"); let state = Arc::new(AppState::new(storage)); state .configs diff --git a/crates/dbx-core/tests/mongodb_column_metadata.rs b/crates/dbx-core/tests/mongodb_column_metadata.rs index 0569378382..8dca6691d6 100644 --- a/crates/dbx-core/tests/mongodb_column_metadata.rs +++ b/crates/dbx-core/tests/mongodb_column_metadata.rs @@ -11,12 +11,11 @@ use dbx_core::db::mongo_driver; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; use dbx_core::mongo_ops::mongo_run_command_core; use dbx_core::schema::get_columns_core; -use dbx_core::storage::Storage; use serde_json::json; async fn state_with_pool(pool: PoolKind, db_type: DatabaseType, database: &str) -> (Arc, tempfile::TempDir) { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); let config: ConnectionConfig = serde_json::from_value(json!({ "id": "metadata", "name": "Column metadata regression", "db_type": db_type, diff --git a/crates/dbx-core/tests/mongodb_dump_restore.rs b/crates/dbx-core/tests/mongodb_dump_restore.rs index 5ebc3d0cad..e5ff1bb62a 100644 --- a/crates/dbx-core/tests/mongodb_dump_restore.rs +++ b/crates/dbx-core/tests/mongodb_dump_restore.rs @@ -186,7 +186,7 @@ async fn official_tools_round_trip_through_dbx_core() { let directory = tempfile::tempdir().unwrap(); let client = mongodb::Client::with_uri_str(&uri).await.unwrap(); let database = format!("dbx_dump_test_{}", uuid::Uuid::new_v4().simple()); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = AppState::new(storage); let connection_id = "mongo-dump-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ @@ -403,7 +403,8 @@ async fn official_database_tools_round_trip_through_dbx() { db.run_command(doc! { "create": "z_view", "viewOn": "records", "pipeline": [ { "$match": { "score": { "$gte": 1 } } } ], "collation": { "locale": "en", "strength": 2 } }).await.unwrap(); db.run_command(doc! { "create": "a_nested", "viewOn": "z_view", "pipeline": [], "collation": { "locale": "en", "strength": 2 } }).await.unwrap(); let collections = ["records", "empty", "capped", "odd/name", "z_view", "a_nested"]; - let state = AppState::new(Storage::open(&files.path().join("storage.db")).await.unwrap()); + let state = + AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("storage.db")).await.unwrap()); let connection_id = "database-dump-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": connection_id, "name": "Database dump test", "db_type": "mongodb", "host": "127.0.0.1", "port": 27090, "username": "", "password": "", "database": database, "connection_string": uri, "driver_profile": "mongodb-native" })).unwrap(); state.configs.write().await.insert(connection_id.into(), config); @@ -776,7 +777,8 @@ async fn legacy_agent_database_round_trip_through_dbx() { let (host, port) = endpoint.split_once(':').expect("host:port"); let files = tempfile::tempdir().unwrap(); let database = format!("dbx_legacy_{}", uuid::Uuid::new_v4().simple()); - let state = AppState::new(Storage::open(&files.path().join("storage.db")).await.unwrap()); + let state = + AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("storage.db")).await.unwrap()); let id = "legacy-dump-test"; let config: ConnectionConfig = serde_json::from_value(serde_json::json!({ "id": id, "name": "Legacy dump test", "db_type": "mongodb", "host": host, "port": port.parse::().unwrap(), "username": "", "password": "", "database": database, "driver_profile": "mongodb-legacy" })).unwrap(); state.configs.write().await.insert(id.into(), config); @@ -961,7 +963,7 @@ async fn directory_preview_does_not_read_bson_and_source_identity_is_checked_bef .unwrap(); assert_eq!(preview.catalog.collections[0].documents, None); assert_eq!(preview.catalog.collections[0].size_bytes, 16); - let state = AppState::new(Storage::open(&files.path().join("state.db")).await.unwrap()); + let state = AppState::new(dbx_core::persistence::test_storage::open(&files.path().join("state.db")).await.unwrap()); let request: MongoDatabaseRestoreRequest = serde_json::from_value(serde_json::json!({ "taskId":"validation-test", "connectionId":"missing", "database":"target", "sourceDatabase":"source", "sourceRef":preview.source_ref, "dropExisting":true diff --git a/crates/dbx-core/tests/mq_token.rs b/crates/dbx-core/tests/mq_token.rs index b114fba9db..c123b74e6f 100644 --- a/crates/dbx-core/tests/mq_token.rs +++ b/crates/dbx-core/tests/mq_token.rs @@ -5,7 +5,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use dbx_core::mq::token::{sign_pulsar_token, token_fingerprint}; use dbx_core::mq::{MqTokenIssueRequest, MqTokenRecord, MqTokenSigningAlgorithm, MqTokenSigningConfig}; -use dbx_core::storage::Storage; #[test] fn hs256_token_contains_pulsar_subject_and_expiry() { @@ -45,7 +44,7 @@ fn token_signing_requires_configured_key() { #[tokio::test] async fn token_records_are_saved_without_token_plaintext() { - let storage = Storage::open(&temp_db_path("mq-token-records")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&temp_db_path("mq-token-records")).await.unwrap(); let record = MqTokenRecord { id: "rec-1".to_string(), connection_id: "conn-1".to_string(), diff --git a/crates/dbx-web/Cargo.toml b/crates/dbx-web/Cargo.toml index 5f2dd509cf..cc7164ad2a 100644 --- a/crates/dbx-web/Cargo.toml +++ b/crates/dbx-web/Cargo.toml @@ -51,4 +51,5 @@ mime_guess = { version = "2", optional = true } [dev-dependencies] +dbx-core = { path = "../dbx-core", default-features = false, features = ["test-support"] } reqwest = { version = "0.12", default-features = false, features = ["multipart"] } diff --git a/crates/dbx-web/src/demo.rs b/crates/dbx-web/src/demo.rs index 4b99516b02..51a2bcef10 100644 --- a/crates/dbx-web/src/demo.rs +++ b/crates/dbx-web/src/demo.rs @@ -310,7 +310,8 @@ mod tests { #[tokio::test] async fn demo_connect_guard_requires_a_pool_equivalent_stored_connection() { let directory = tempfile::tempdir().unwrap(); - let storage = dbx_core::storage::Storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); + let storage = + dbx_core::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); let app = std::sync::Arc::new(dbx_core::connection::AppState::new(storage)); let unknown = stored_config(); @@ -334,7 +335,8 @@ mod tests { use axum::routing::{get, post}; let directory = tempfile::tempdir().unwrap(); - let storage = dbx_core::storage::Storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); + let storage = + dbx_core::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); let app = std::sync::Arc::new(dbx_core::connection::AppState::new(storage)); let state = { let mut web_state = crate::state::WebState::for_tests(app, directory.path().to_path_buf()); @@ -364,7 +366,7 @@ mod tests { // 关闭开关后同一中间件直接放行。 let app = std::sync::Arc::new(dbx_core::connection::AppState::new( - dbx_core::storage::Storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(), + dbx_core::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(), )); let state = std::sync::Arc::new(crate::state::WebState::for_tests(app, directory.path().to_path_buf())); let router = axum::Router::new() diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index d474a57bb6..034f29be15 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -1452,7 +1452,8 @@ mod tests { async fn migration_http_gate_blocks_business_until_ready_but_allows_cleanup_handler() { use std::sync::{atomic::Ordering, Arc}; let directory = tempfile::tempdir().unwrap(); - let storage = dbx_core::storage::Storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); + let storage = + dbx_core::persistence::test_storage::open_unmigrated(&directory.path().join("dbx.db")).await.unwrap(); let app = Arc::new(dbx_core::connection::AppState::new(storage)); let state = Arc::new(crate::state::WebState::for_tests(app, directory.path().to_path_buf())); state.migration_ready.store(false, Ordering::Release); diff --git a/crates/dbx-web/src/routes/ai.rs b/crates/dbx-web/src/routes/ai.rs index 9007617faa..93fc67f777 100644 --- a/crates/dbx-web/src/routes/ai.rs +++ b/crates/dbx-web/src/routes/ai.rs @@ -659,7 +659,7 @@ mod tests { let dir = std::env::temp_dir().join(format!("dbx-web-intg-max-retries-{}", uuid::Uuid::new_v4())); let _ = std::fs::create_dir_all(&dir); - let storage = dbx_core::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); storage.save_max_retries(0).await.unwrap(); assert_eq!(storage.load_max_retries().await.unwrap(), 0); diff --git a/crates/dbx-web/src/routes/cloud_sync.rs b/crates/dbx-web/src/routes/cloud_sync.rs index d17b82b810..8a55f1cf80 100644 --- a/crates/dbx-web/src/routes/cloud_sync.rs +++ b/crates/dbx-web/src/routes/cloud_sync.rs @@ -387,7 +387,6 @@ mod tests { use axum::Json; use dbx_core::cloud_sync::SnippetProvider; use dbx_core::connection::AppState; - use dbx_core::storage::Storage; use crate::state::WebState; @@ -396,11 +395,11 @@ mod tests { let dir = std::env::temp_dir().join(format!("dbx-web-snippet-cleanup-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); let db = dir.join("storage.db"); - let storage = Storage::open(&db).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&db).await.unwrap(); storage.save_snippet_migration_state("github", "replacement-id", "legacy-id", "content-hash").await.unwrap(); drop(storage); - let storage = Storage::open(&db).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&db).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); let Json(settings) = super::snippet_sync_settings( diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index 0bbf57b2e9..6398f580d4 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -972,7 +972,7 @@ mod tests { use dbx_core::nacos::config::{ NacosAuthConfig, NacosRNacosConsoleAuth, NACOS_CONSOLE_SESSION_PASSWORD, NACOS_PRIMARY_SESSION_PASSWORD, }; - use dbx_core::storage::{McpGlobalPolicy, Storage}; + use dbx_core::storage::McpGlobalPolicy; use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; @@ -1180,7 +1180,7 @@ mod tests { async fn test_web_state() -> (Arc, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-web-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); (state, dir) @@ -1697,7 +1697,7 @@ mod tests { .await }) .await; - assert!(result.is_ok()); + assert!(result.is_ok(), "{result:?}"); // 全局运行态配置不含明文密码(泄露面消除)。 let stored = state.app.configs.read().await.get("conn-a").cloned().unwrap(); diff --git a/crates/dbx-web/src/routes/mongo.rs b/crates/dbx-web/src/routes/mongo.rs index 151ed71733..6b757769a5 100644 --- a/crates/dbx-web/src/routes/mongo.rs +++ b/crates/dbx-web/src/routes/mongo.rs @@ -1023,7 +1023,7 @@ mod tests { }; use dbx_core::connection::AppState; use dbx_core::models::connection::ConnectionConfig; - use dbx_core::storage::{McpGlobalPolicy, Storage}; + use dbx_core::storage::McpGlobalPolicy; use std::sync::Arc; fn mongo_config(is_production: bool) -> ConnectionConfig { @@ -1044,7 +1044,7 @@ mod tests { async fn test_web_state() -> (Arc, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-web-mongo-policy-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); (state, dir) diff --git a/crates/dbx-web/src/routes/mongodb_import_export.rs b/crates/dbx-web/src/routes/mongodb_import_export.rs index d3286e2b43..a2c8c2de38 100644 --- a/crates/dbx-web/src/routes/mongodb_import_export.rs +++ b/crates/dbx-web/src/routes/mongodb_import_export.rs @@ -613,11 +613,12 @@ mod tests { #[tokio::test] async fn export_failure_and_cancellation_are_replayed_to_late_subscribers() { use axum::response::IntoResponse; - use dbx_core::{connection::AppState, storage::Storage}; + use dbx_core::connection::AppState; for cancelled in [false, true] { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = + dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(WebState::for_tests(Arc::new(AppState::new(storage)), directory.path().into())); let id = uuid::Uuid::new_v4().to_string(); let expected = if cancelled { MongoExportStatus::Cancelled } else { MongoExportStatus::Error }; @@ -674,10 +675,10 @@ mod tests { #[tokio::test] async fn early_import_failure_finishes_progress_and_cleans_source() { - use dbx_core::{connection::AppState, storage::Storage}; + use dbx_core::connection::AppState; let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("storage.db")).await.unwrap(); let state = Arc::new(WebState::for_tests(Arc::new(AppState::new(storage)), directory.path().into())); let upload_dir = import_upload_dir(directory.path()); std::fs::create_dir_all(&upload_dir).unwrap(); diff --git a/crates/dbx-web/src/routes/mq.rs b/crates/dbx-web/src/routes/mq.rs index 11fef9acb6..dcc12976c3 100644 --- a/crates/dbx-web/src/routes/mq.rs +++ b/crates/dbx-web/src/routes/mq.rs @@ -1554,7 +1554,7 @@ mod tests { async fn test_web_state() -> (Arc, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-web-mq-policy-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState { app, diff --git a/crates/dbx-web/src/routes/object_cache_tests.rs b/crates/dbx-web/src/routes/object_cache_tests.rs index 52eba664a2..eacae3394f 100644 --- a/crates/dbx-web/src/routes/object_cache_tests.rs +++ b/crates/dbx-web/src/routes/object_cache_tests.rs @@ -6,11 +6,8 @@ use axum::{ Router, }; use dbx_core::{ - agent_tools::AgentSqlPermissions, - connection::AppState, - models::connection::ConnectionConfig, - query::QueryExecutionOptions, - storage::{McpGlobalPolicy, Storage}, + agent_tools::AgentSqlPermissions, connection::AppState, models::connection::ConnectionConfig, + query::QueryExecutionOptions, storage::McpGlobalPolicy, }; use dbx_mcp::{DbxBackend, LocalBackend, WebBackend}; use serde_json::{json, Value}; @@ -19,7 +16,7 @@ use std::sync::Arc; async fn run_object_cache_regression(postgres: bool) { let dir = std::env::temp_dir().join(format!("dbx-object-cache-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let database = if postgres { std::env::var("DBX_LIVE_POSTGRES_DATABASE").expect("test database") } else { "main".into() }; let config: ConnectionConfig = serde_json::from_value(json!({ diff --git a/crates/dbx-web/src/routes/query.rs b/crates/dbx-web/src/routes/query.rs index 3999b3b692..01275beb48 100644 --- a/crates/dbx-web/src/routes/query.rs +++ b/crates/dbx-web/src/routes/query.rs @@ -1311,12 +1311,11 @@ mod tests { use crate::state::WebState; use axum::extract::State as AxumState; use dbx_core::connection::AppState; - use dbx_core::storage::Storage; async fn test_web_state() -> (Arc, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-web-query-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); (state, dir) diff --git a/crates/dbx-web/src/routes/query_result_export.rs b/crates/dbx-web/src/routes/query_result_export.rs index 91c17c2096..a305241939 100644 --- a/crates/dbx-web/src/routes/query_result_export.rs +++ b/crates/dbx-web/src/routes/query_result_export.rs @@ -182,7 +182,6 @@ mod tests { use super::*; use axum::body::to_bytes; use dbx_core::connection::AppState; - use dbx_core::storage::Storage; use crate::state::WebExportFile; @@ -190,7 +189,7 @@ mod tests { async fn query_result_export_download_uses_the_requested_web_filename() { let dir = std::env::temp_dir().join(format!("dbx-web-query-export-download-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); let file_path = dir.join("query-export.csv"); diff --git a/crates/dbx-web/src/routes/sql_file.rs b/crates/dbx-web/src/routes/sql_file.rs index af4fc796a0..eaa7da2891 100644 --- a/crates/dbx-web/src/routes/sql_file.rs +++ b/crates/dbx-web/src/routes/sql_file.rs @@ -499,7 +499,7 @@ mod tests { use super::*; async fn restore_state(directory: &Path) -> WebState { - let storage = dbx_core::storage::Storage::open(&directory.join("dbx.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.join("dbx.db")).await.unwrap(); WebState::for_tests(Arc::new(dbx_core::connection::AppState::new(storage)), directory.to_path_buf()) } diff --git a/crates/dbx-web/src/routes/table_export.rs b/crates/dbx-web/src/routes/table_export.rs index c9070e15ca..5d8a2039d7 100644 --- a/crates/dbx-web/src/routes/table_export.rs +++ b/crates/dbx-web/src/routes/table_export.rs @@ -170,7 +170,6 @@ mod tests { use super::*; use axum::body::to_bytes; use dbx_core::connection::AppState; - use dbx_core::storage::Storage; use crate::state::WebExportFile; @@ -178,7 +177,7 @@ mod tests { async fn table_export_download_uses_the_requested_web_filename() { let dir = std::env::temp_dir().join(format!("dbx-web-table-export-download-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); let file_path = dir.join("table-export.xlsx"); diff --git a/crates/dbx-web/src/routes/transfer.rs b/crates/dbx-web/src/routes/transfer.rs index 7e307db251..45659d837e 100644 --- a/crates/dbx-web/src/routes/transfer.rs +++ b/crates/dbx-web/src/routes/transfer.rs @@ -667,7 +667,6 @@ mod tests { default_connect_timeout_secs, default_idle_timeout_secs, default_keepalive_interval_secs, default_query_timeout_secs, ConnectionConfig, DatabaseType, }; - use dbx_core::storage::Storage; use dbx_core::transfer::{ TransferContent, TransferMode, TransferOwnershipPolicy, TransferRequest, TransferTableNameCase, }; @@ -742,7 +741,7 @@ mod tests { async fn test_web_state() -> (Arc, std::path::PathBuf) { let dir = std::env::temp_dir().join(format!("dbx-web-transfer-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let app = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let state = Arc::new(WebState::for_tests(app, dir.clone())); (state, dir) diff --git a/package.json b/package.json index bdc5db4096..6a17adcd8a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "dev": "vite --config apps/desktop/vite.config.ts", - "dev:tauri": "tauri dev", + "dev:tauri": "node scripts/dev-tauri.mjs", "dev:web": "vite --config apps/desktop/vite.config.ts --port 5173 --mode web", "dev:backend": "node scripts/dev-backend.mjs", "dev:full:win": ".\\scripts\\dev-full.bat", diff --git a/scripts/dev-tauri.mjs b/scripts/dev-tauri.mjs new file mode 100644 index 0000000000..774b147b16 --- /dev/null +++ b/scripts/dev-tauri.mjs @@ -0,0 +1,73 @@ +import { spawn } from "node:child_process"; +import { createRequire } from "node:module"; +import { realpathSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { ensureSigningIdentity, signDevelopmentBinary } from "./macos-dev-signing.mjs"; + +const entryPath = fileURLToPath(import.meta.url); + +export function developmentArgs(args, platform = process.platform, executable = process.execPath, runner = entryPath) { + if (platform !== "darwin") return ["dev", ...args]; + const separator = args.indexOf("--"); + const tauriArgs = separator < 0 ? args : args.slice(0, separator); + const cargoArgs = separator < 0 ? [] : args.slice(separator + 1); + const binarySeparator = cargoArgs.indexOf("--"); + const buildArgs = binarySeparator < 0 ? cargoArgs : cargoArgs.slice(0, binarySeparator); + const binaryArgs = binarySeparator < 0 ? [] : cargoArgs.slice(binarySeparator); + if ([...tauriArgs, ...buildArgs].includes("--release")) throw new Error("Use tauri build for release builds; the development signer must not sign release artifacts."); + if (tauriArgs.some((arg) => arg === "-r" || arg === "--runner" || arg.startsWith("--runner="))) throw new Error("Custom Tauri runners bypass development signing; use the default Cargo runner."); + const runnerValue = JSON.stringify([executable, runner, "--run-signed"]); + const configs = ["aarch64-apple-darwin", "x86_64-apple-darwin"].flatMap((target) => ["--config", `target.${target}.runner=${runnerValue}`]); + return ["dev", ...tauriArgs, "--", ...buildArgs, ...configs, ...binaryArgs]; +} + +function run(executable, args) { + const child = spawn(executable, args, { stdio: "inherit", env: process.env }); + const handlers = new Map(); + for (const signal of ["SIGINT", "SIGTERM"]) { + const handler = () => child.kill(signal); + handlers.set(signal, handler); + process.on(signal, handler); + } + child.on("error", (error) => { + console.error(error.message); + process.exitCode = 1; + }); + child.on("close", (code, signal) => { + for (const [name, handler] of handlers) process.removeListener(name, handler); + if (signal) process.kill(process.pid, signal); + else process.exitCode = code ?? 1; + }); +} + +export function main(args = process.argv.slice(2)) { + if (args[0] === "--run-signed") { + if (process.platform !== "darwin" || !args[1]) throw new Error("The signing runner requires a macOS development executable."); + const binary = realpathSync.native(args[1]); + if (path.basename(binary) !== "dbx" || path.basename(path.dirname(binary)) !== "debug") { + throw new Error("The development signing runner only accepts a debug/dbx executable."); + } + signDevelopmentBinary(binary); + run(binary, args.slice(2)); + return; + } + const tauriArgs = developmentArgs(args); + if (process.platform === "darwin" && !args.some((arg) => ["--help", "-h", "--version", "-V"].includes(arg))) { + for (const target of ["AARCH64_APPLE_DARWIN", "X86_64_APPLE_DARWIN"]) { + if (process.env[`CARGO_TARGET_${target}_RUNNER`]) throw new Error(`Unset CARGO_TARGET_${target}_RUNNER before using the DBX development signing runner.`); + } + ensureSigningIdentity(); + } + const tauri = createRequire(import.meta.url).resolve("@tauri-apps/cli/tauri.js"); + run(process.execPath, [tauri, ...tauriArgs]); +} + +if (process.argv[1] && realpathSync.native(process.argv[1]) === realpathSync.native(entryPath)) { + try { + main(); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/scripts/dev-tauri.test.mjs b/scripts/dev-tauri.test.mjs new file mode 100644 index 0000000000..027c63954d --- /dev/null +++ b/scripts/dev-tauri.test.mjs @@ -0,0 +1,119 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { developmentArgs } from "./dev-tauri.mjs"; +import { designatedRequirement, ensureSigningIdentity, loadSigningIdentity, removeSigningKeychain, signDevelopmentBinary } from "./macos-dev-signing.mjs"; +import { fileURLToPath } from "node:url"; + +test("non-macOS development arguments are unchanged", () => { + for (const platform of ["linux", "win32"]) { + assert.deepEqual(developmentArgs(["--", "--no-default-features"], platform), ["dev", "--", "--no-default-features"]); + } +}); + +test("both macOS targets receive a Cargo runner with space-safe paths", () => { + const args = developmentArgs([], "darwin", "/path with spaces/node", "/repo with spaces/dev.mjs"); + assert.deepEqual(args.slice(0, 2), ["dev", "--"]); + for (const [index, target] of [ + [3, "aarch64"], + [5, "x86_64"], + ]) { + assert.equal(args[index], `target.${target}-apple-darwin.runner=["/path with spaces/node","/repo with spaces/dev.mjs","--run-signed"]`); + } +}); + +test("Tauri flags, Cargo features and application arguments keep their separators", () => { + const args = developmentArgs(["--no-watch", "--", "--no-default-features", "--features", "sqlite-bundled,os-keyring", "--", "--example"], "darwin"); + assert.deepEqual(args.slice(0, 6), ["dev", "--no-watch", "--", "--no-default-features", "--features", "sqlite-bundled,os-keyring"]); + assert.deepEqual(args.slice(-2), ["--", "--example"]); +}); + +test("release builds never enter the development signing flow", () => { + assert.throws(() => developmentArgs(["--release"], "darwin"), /release artifacts/); + assert.deepEqual(developmentArgs(["--", "--", "--release"], "darwin").slice(-2), ["--", "--release"]); +}); + +test("custom Tauri runners cannot bypass development signing", () => { + for (const args of [["-r", "custom"], ["--runner", "custom"], ["--runner=custom"]]) { + assert.throws(() => developmentArgs(args, "darwin"), /bypass development signing/); + } +}); + +test("incomplete identities are preserved instead of silently regenerated", { skip: process.platform !== "darwin" }, () => { + const directory = mkdtempSync(path.join(tmpdir(), "dbx-dev-signing-incomplete-")); + const keychain = path.join(directory, "development.keychain-db"); + try { + writeFileSync(keychain, "existing identity", { mode: 0o600 }); + assert.throws(() => ensureSigningIdentity(directory), /Incomplete development signing setup/); + assert.equal(readFileSync(keychain, "utf8"), "existing identity"); + assert.equal(existsSync(path.join(directory, "setup.lock")), false); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("the designated requirement binds both the stable identifier and certificate", () => { + const fingerprint = "AB".repeat(20); + assert.equal(designatedRequirement(fingerprint), `identifier "com.dbx.app.development" and certificate leaf = H"${fingerprint}"`); + for (const invalid of ["", "-", "AB", '" or true']) assert.throws(() => designatedRequirement(invalid)); +}); + +test("signing configuration rejects symlinked directories before accessing a keychain", { skip: process.platform !== "darwin" }, () => { + const root = mkdtempSync(path.join(tmpdir(), "dbx-dev-signing-test-")); + try { + mkdirSync(path.join(root, "real")); + symlinkSync(path.join(root, "real"), path.join(root, "link")); + assert.throws(() => loadSigningIdentity(path.join(root, "link")), /unsafe path/); + assert.throws(() => ensureSigningIdentity(path.join(root, "link")), /unsafe path/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test( + "rebuilt binaries retain Keychain access without user interaction", + { + skip: process.platform !== "darwin" || process.env.DBX_TEST_MACOS_KEYCHAIN !== "1", + timeout: 60000, + }, + () => { + const root = mkdtempSync(path.join(tmpdir(), "dbx-dev-signing-integration-")); + const directory = path.join(root, "signing"); + let identity; + const execute = (command, args) => execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); + try { + identity = ensureSigningIdentity(directory); + const binary = path.join(root, "probe"); + const source = fileURLToPath(new URL("./fixtures/macos-dev-keychain.c", import.meta.url)); + const compile = (version) => execute("/usr/bin/clang", ["-Wno-deprecated-declarations", `-DDBX_PROBE_VERSION=${version}`, source, "-framework", "Security", "-framework", "CoreFoundation", "-o", binary]); + const signature = () => { + const result = spawnSync("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", binary], { encoding: "utf8" }); + assert.equal(result.status, 0); + const output = result.stdout + result.stderr; + return { requirement: output.match(/designated => (.+)/)?.[1], hash: output.match(/^CDHash=(.+)$/m)?.[1] }; + }; + compile(1); + signDevelopmentBinary(binary, directory); + const first = signature(); + assert.match(execute(binary, [identity.keychain]), /build=1 status=0/); + compile(2); + const unsigned = spawnSync(binary, [identity.keychain], { encoding: "utf8" }); + assert.equal(unsigned.status, 3, "an ad-hoc rebuild must not inherit the signed program's authorization"); + signDevelopmentBinary(binary, directory); + const second = signature(); + const searchList = execute("/usr/bin/security", ["list-keychains", "-d", "user"]); + assert.equal([...searchList.matchAll(/"([^"\n]+)"/g)].filter((match) => match[1] === identity.keychain).length, 1); + assert.ok(first.hash && first.requirement); + assert.notEqual(first.hash, second.hash); + assert.equal(first.requirement, second.requirement); + assert.match(execute(binary, [identity.keychain]), /build=2 status=0/); + assert.equal(ensureSigningIdentity(directory).fingerprint, identity.fingerprint); + } finally { + if (identity) removeSigningKeychain(identity.keychain); + rmSync(root, { recursive: true, force: true }); + } + }, +); diff --git a/scripts/fixtures/macos-dev-keychain.c b/scripts/fixtures/macos-dev-keychain.c new file mode 100644 index 0000000000..52d018ed4e --- /dev/null +++ b/scripts/fixtures/macos-dev-keychain.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc != 2) return 1; + if (SecKeychainSetUserInteractionAllowed(false) != errSecSuccess) return 4; + SecKeychainRef keychain = NULL; + OSStatus status = SecKeychainOpen(argv[1], &keychain); + if (status != errSecSuccess) return 2; + const char *service = "com.dbx.tests.development-signing"; + const char *account = "fixture"; + const char *expected = "fixture-only-value"; + UInt32 length = 0; + void *password = NULL; + status = SecKeychainFindGenericPassword(keychain, (UInt32)strlen(service), service, + (UInt32)strlen(account), account, &length, &password, NULL); + if (status == errSecItemNotFound) { + status = SecKeychainAddGenericPassword(keychain, (UInt32)strlen(service), service, + (UInt32)strlen(account), account, (UInt32)strlen(expected), expected, NULL); + } else if (status == errSecSuccess) { + if (length != strlen(expected) || memcmp(password, expected, length) != 0) status = errSecDecode; + SecKeychainItemFreeContent(NULL, password); + } + CFRelease(keychain); + printf("build=%d status=%d\n", DBX_PROBE_VERSION, (int)status); + return status == errSecSuccess ? 0 : 3; +} diff --git a/scripts/macos-dev-signing.mjs b/scripts/macos-dev-signing.mjs new file mode 100644 index 0000000000..ccb8203369 --- /dev/null +++ b/scripts/macos-dev-signing.mjs @@ -0,0 +1,160 @@ +import { execFileSync } from "node:child_process"; +import { randomBytes, X509Certificate } from "node:crypto"; +import { chmodSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +export const developmentIdentifier = "com.dbx.app.development"; + +export function signingDirectory() { + return path.join(homedir(), "Library", "Application Support", "DBX", "development-signing"); +} + +function command(executable, args, options = {}) { + try { + return execFileSync(executable, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + ...options, + }); + } catch (error) { + throw new Error(`Development signing: ${path.basename(executable)} ${args[0]} failed (${error.status ?? error.code}).`); + } +} + +function privatePath(file, directory = false) { + const metadata = lstatSync(file); + if (metadata.isSymbolicLink() || (directory ? !metadata.isDirectory() : !metadata.isFile()) || metadata.uid !== process.getuid()) { + throw new Error(`Development signing refuses an unsafe path: ${file}`); + } + chmodSync(file, directory ? 0o700 : 0o600); +} + +function keychainSearchList() { + const output = command("/usr/bin/security", ["list-keychains", "-d", "user"]); + return [...output.matchAll(/"([^"\n]+)"/g)].map((match) => match[1]); +} + +function canonicalKeychain(keychain) { + return existsSync(keychain) ? realpathSync.native(keychain) : keychain; +} + +function includeKeychain(keychain) { + const canonical = canonicalKeychain(keychain); + const keychains = keychainSearchList(); + const updated = [...keychains.filter((entry) => canonicalKeychain(entry) !== canonical), canonical]; + if (JSON.stringify(updated) !== JSON.stringify(keychains)) { + command("/usr/bin/security", ["list-keychains", "-d", "user", "-s", ...updated]); + } +} + +export function removeSigningKeychain(keychain) { + const canonical = canonicalKeychain(keychain); + const keychains = keychainSearchList(); + const updated = keychains.filter((entry) => canonicalKeychain(entry) !== canonical); + if (updated.length !== keychains.length) { + command("/usr/bin/security", ["list-keychains", "-d", "user", "-s", ...updated]); + } + command("/usr/bin/security", ["delete-keychain", canonical]); +} + +export function designatedRequirement(fingerprint) { + if (!/^[A-F0-9]{40}$/.test(fingerprint)) { + throw new Error("Invalid development certificate fingerprint."); + } + return `identifier "${developmentIdentifier}" and certificate leaf = H"${fingerprint}"`; +} + +export function loadSigningIdentity(directory = signingDirectory()) { + privatePath(directory, true); + directory = realpathSync.native(directory); + const configPath = path.join(directory, "identity.json"); + const certificatePath = path.join(directory, "certificate.pem"); + const keychain = path.join(directory, "development.keychain-db"); + for (const file of [configPath, certificatePath, keychain]) privatePath(file); + let identity; + try { + identity = JSON.parse(readFileSync(configPath, "utf8")); + } catch { + throw new Error("Invalid development signing configuration; the existing identity was preserved."); + } + if (identity?.version !== 1 || !/^[a-f0-9]{64}$/.test(identity.password)) { + throw new Error("Invalid development signing configuration; do not delete or regenerate an existing identity automatically."); + } + designatedRequirement(identity.fingerprint); + const certificate = new X509Certificate(readFileSync(certificatePath)); + if (certificate.fingerprint.replaceAll(":", "") !== identity.fingerprint || Date.parse(certificate.validTo) <= Date.now()) { + throw new Error("The development certificate is mismatched or expired; replace it explicitly and authorize the new identity once."); + } + return { ...identity, keychain }; +} + +export function ensureSigningIdentity(directory = signingDirectory()) { + mkdirSync(directory, { recursive: true, mode: 0o700 }); + privatePath(directory, true); + directory = realpathSync.native(directory); + const configPath = path.join(directory, "identity.json"); + if (existsSync(configPath)) return loadSigningIdentity(directory); + const lockPath = path.join(directory, "setup.lock"); + let lock; + try { + lock = openSync(lockPath, "wx", 0o600); + } catch { + throw new Error(`Development signing setup is already running or was interrupted. Check ${lockPath} before retrying.`); + } + const keychain = path.join(directory, "development.keychain-db"); + let temporary; + let createdKeychain = false; + try { + if (existsSync(configPath)) return loadSigningIdentity(directory); + if (existsSync(keychain) || existsSync(path.join(directory, "certificate.pem"))) { + throw new Error(`Incomplete development signing setup in ${directory}; refusing to replace an existing identity.`); + } + temporary = mkdtempSync(path.join(directory, "setup-")); + const password = randomBytes(32).toString("hex"); + const config = path.join(temporary, "openssl.cnf"); + const privateKey = path.join(temporary, "private.pem"); + const certificatePath = path.join(temporary, "certificate.pem"); + const bundle = path.join(temporary, "identity.p12"); + writeFileSync(config, ["[req]", "prompt=no", "distinguished_name=subject", "x509_extensions=extensions", "[subject]", "CN=DBX Local Development", "[extensions]", "basicConstraints=critical,CA:FALSE", "keyUsage=critical,digitalSignature", "extendedKeyUsage=critical,codeSigning", ""].join("\n"), { + mode: 0o600, + flag: "wx", + }); + command("/usr/bin/openssl", ["req", "-x509", "-newkey", "rsa:3072", "-nodes", "-sha256", "-days", "3650", "-config", config, "-keyout", privateKey, "-out", certificatePath]); + command("/usr/bin/openssl", ["pkcs12", "-export", "-inkey", privateKey, "-in", certificatePath, "-out", bundle, "-passout", "env:DBX_DEV_SIGNING_PASSWORD"], { + env: { ...process.env, DBX_DEV_SIGNING_PASSWORD: password }, + }); + command("/usr/bin/security", ["create-keychain", "-p", password, keychain]); + createdKeychain = true; + command("/usr/bin/security", ["set-keychain-settings", "-lut", "21600", keychain]); + command("/usr/bin/security", ["unlock-keychain", "-p", password, keychain]); + command("/usr/bin/security", ["import", bundle, "-k", keychain, "-P", password, "-T", "/usr/bin/codesign"]); + command("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychain]); + includeKeychain(keychain); + const certificate = readFileSync(certificatePath); + const fingerprint = new X509Certificate(certificate).fingerprint.replaceAll(":", ""); + writeFileSync(path.join(directory, "certificate.pem"), certificate, { mode: 0o600, flag: "wx" }); + writeFileSync(configPath, JSON.stringify({ version: 1, fingerprint, password }) + "\n", { mode: 0o600, flag: "wx" }); + console.error("Created a local DBX development signing identity; release certificates and connection keys were not changed."); + return loadSigningIdentity(directory); + } catch (error) { + if (createdKeychain && !existsSync(configPath)) { + removeSigningKeychain(keychain); + rmSync(path.join(directory, "certificate.pem"), { force: true }); + } + throw error; + } finally { + if (temporary) rmSync(temporary, { recursive: true, force: true }); + closeSync(lock); + rmSync(lockPath); + } +} + +export function signDevelopmentBinary(binary, directory = signingDirectory()) { + const identity = loadSigningIdentity(directory); + const requirement = designatedRequirement(identity.fingerprint); + command("/usr/bin/security", ["unlock-keychain", "-p", identity.password, identity.keychain]); + includeKeychain(identity.keychain); + command("/usr/bin/codesign", ["--force", "--sign", identity.fingerprint, "--keychain", identity.keychain, "--identifier", developmentIdentifier, "--requirements", `=designated => ${requirement}`, "--preserve-metadata=entitlements", "--timestamp=none", binary]); + command("/usr/bin/codesign", ["--verify", "--strict", "-R", `=${requirement}`, binary]); +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d9c7a16b4e..13b65861f8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -80,6 +80,7 @@ sys-locale = "0.3" sysinfo = { version = "0.32", features = ["system"] } [dev-dependencies] +dbx-core = { path = "../crates/dbx-core", default-features = false, features = ["test-support"] } tauri = { version = "2.10.3", features = ["test"] } tempfile = "3" diff --git a/src-tauri/src/background_backup.rs b/src-tauri/src/background_backup.rs index f034b78311..561a66d632 100644 --- a/src-tauri/src/background_backup.rs +++ b/src-tauri/src/background_backup.rs @@ -487,7 +487,7 @@ mod tests { #[tokio::test] async fn shutdown_stops_the_mock_supervisor_and_removes_only_its_lease() { let directory = tempfile::tempdir().unwrap(); - let storage = Storage::open(&directory.path().join("dbx.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&directory.path().join("dbx.db")).await.unwrap(); let state = Arc::new(AppState::new(storage)); std::fs::create_dir_all(directory.path().join("database-backups")).unwrap(); std::fs::write(marker(directory.path()), b"1").unwrap(); diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index dbdbf26cc4..ae657359e5 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -835,7 +835,7 @@ mod tests { async fn tauri_entry_respects_global_max_retries_zero() { let dir = std::env::temp_dir().join(format!("dbx-tauri-mr-{}", uuid::Uuid::new_v4())); let _ = std::fs::create_dir_all(&dir); - let storage = dbx_core::storage::Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); storage.save_max_retries(0).await.unwrap(); assert_eq!(storage.load_max_retries().await.unwrap(), 0); diff --git a/src-tauri/src/commands/cloud_sync.rs b/src-tauri/src/commands/cloud_sync.rs index 62ae9382c7..4d8f4a2e40 100644 --- a/src-tauri/src/commands/cloud_sync.rs +++ b/src-tauri/src/commands/cloud_sync.rs @@ -311,7 +311,6 @@ mod tests { use std::sync::Arc; use dbx_core::cloud_sync::SnippetProvider; - use dbx_core::storage::Storage; use tauri::Manager; use super::AppState; @@ -321,11 +320,11 @@ mod tests { let dir = std::env::temp_dir().join(format!("dbx-tauri-snippet-cleanup-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); let db = dir.join("storage.db"); - let storage = Storage::open(&db).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&db).await.unwrap(); storage.save_snippet_migration_state("github", "replacement-id", "legacy-id", "content-hash").await.unwrap(); drop(storage); - let storage = Storage::open(&db).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&db).await.unwrap(); let app_state = Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))); let app = tauri::test::mock_app(); app.manage(app_state); diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index 355f58aa59..253d400567 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -220,7 +220,6 @@ mod tests { }; use dbx_core::connection::{AppState, PoolKind}; use dbx_core::models::connection::{AttachedDatabaseConfig, ConnectionConfig, DatabaseType}; - use dbx_core::storage::Storage; fn mongodb_config() -> ConnectionConfig { ConnectionConfig { @@ -414,7 +413,7 @@ mod tests { async fn saving_memory_sqlite_attachments_keeps_the_live_pool_intact() { let dir = std::env::temp_dir().join(format!("dbx-tauri-sqlite-memory-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let initial = sqlite_config(std::path::Path::new(":memory:"), ""); let pool = dbx_core::db::sqlite::connect_path(":memory:").await.unwrap(); @@ -578,7 +577,7 @@ mod tests { async fn persist_mongo_legacy_driver_profile_updates_only_the_target_connection() { let dir = std::env::temp_dir().join(format!("dbx-tauri-mongo-profile-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let mongo = mongodb_config(); let mut other = mongodb_config(); @@ -647,7 +646,7 @@ mod tests { async fn save_connection_configs_updates_runtime_cache_and_drops_mq_adapter() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let initial = mq_config("mq-conn", "http://127.0.0.1:8080"); state.configs.write().await.insert(initial.id.clone(), initial.clone()); @@ -684,7 +683,7 @@ mod tests { async fn load_connection_configs_syncs_runtime_cache_and_drops_stale_pool() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let initial = mq_config("mq-conn", "http://127.0.0.1:8080"); let updated = mq_config("mq-conn", "http://127.0.0.1:8081"); @@ -719,7 +718,7 @@ mod tests { async fn save_connection_configs_removes_deleted_runtime_config_and_mq_adapter() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let kept = mongodb_config(); let removed = mq_config("removed-mq", "http://127.0.0.1:8080"); @@ -766,7 +765,7 @@ mod tests { async fn save_connection_configs_retains_one_time_runtime_config_and_its_pool() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let persisted = mongodb_config(); let preview = duckdb_preview_config(); @@ -791,7 +790,7 @@ mod tests { async fn save_connection_configs_keeps_session_credential_of_one_time_config() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let persisted = mongodb_config(); let preview = duckdb_preview_config(); @@ -812,7 +811,7 @@ mod tests { async fn save_connection_configs_removes_deleted_connection_pools() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let kept = mongodb_config(); let removed = mq_config("removed-mq", "http://127.0.0.1:8080"); @@ -838,7 +837,7 @@ mod tests { async fn sync_connection_configs_ignores_password_only_changes() { let dir = std::env::temp_dir().join(format!("dbx-tauri-conn-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let mut initial = mongodb_config(); @@ -871,7 +870,7 @@ mod tests { async fn sync_connection_configs_preserves_nacos_session_password_for_scope_updates() { let dir = std::env::temp_dir().join(format!("dbx-tauri-nacos-scope-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); let state = AppState::new_with_plugin_dir(storage, dir.join("plugins")); let initial = nacos_config("nacos-a"); state.configs.write().await.insert(initial.id.clone(), initial.clone()); diff --git a/src-tauri/src/commands/mcp_bridge.rs b/src-tauri/src/commands/mcp_bridge.rs index 05615a240b..3c350ac1e8 100644 --- a/src-tauri/src/commands/mcp_bridge.rs +++ b/src-tauri/src/commands/mcp_bridge.rs @@ -307,7 +307,7 @@ mod tests { resolve_mongo_database, resolve_mongo_target_values, write_port_file, AppState, }; use dbx_core::models::connection::{ConnectionConfig, DatabaseType}; - use dbx_core::storage::{McpConnectionPolicy, McpDatabasePolicy, McpDatabaseScope, McpGlobalPolicy, Storage}; + use dbx_core::storage::{McpConnectionPolicy, McpDatabasePolicy, McpDatabaseScope, McpGlobalPolicy}; use std::sync::Arc; fn mysql_config(read_only: bool) -> ConnectionConfig { @@ -413,7 +413,7 @@ mod tests { uuid::Uuid::new_v4() )); std::fs::create_dir_all(&root).unwrap(); - let storage = Storage::open(&root.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&root.join("storage.db")).await.unwrap(); let mut config = mysql_config(false); storage.save_connections(&[config.clone()]).await.unwrap(); let state = Arc::new(AppState::new_with_plugin_dir(storage, root.join("plugins"))); @@ -502,7 +502,7 @@ mod tests { uuid::Uuid::new_v4() )); std::fs::create_dir_all(&root).unwrap(); - let storage = Storage::open(&root.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&root.join("storage.db")).await.unwrap(); let mut config = mysql_config(false); config.id = "conn-1".to_string(); storage.save_connections(&[config]).await.unwrap(); diff --git a/src-tauri/src/commands/query.rs b/src-tauri/src/commands/query.rs index 908158f729..e17eb44cc9 100644 --- a/src-tauri/src/commands/query.rs +++ b/src-tauri/src/commands/query.rs @@ -1037,13 +1037,12 @@ pub fn build_create_user_sql(username: String, password: String, tablespace: Str #[cfg(test)] mod tests { use super::*; - use dbx_core::storage::Storage; use std::sync::Arc; async fn test_app_state() -> Arc { let dir = std::env::temp_dir().join(format!("dbx-query-test-{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&dir).unwrap(); - let storage = Storage::open(&dir.join("storage.db")).await.unwrap(); + let storage = dbx_core::persistence::test_storage::open(&dir.join("storage.db")).await.unwrap(); Arc::new(AppState::new_with_plugin_dir(storage, dir.join("plugins"))) }