mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
fix(database): fail closed across restore migrations
Classify every historical migration boundary, thread the restore context through the full chain, reject repair or synthesis for untrusted inputs, and add SQL/binary public-entry certification for damaged and valid legacy fixtures.
This commit is contained in:
@@ -0,0 +1,496 @@
|
|||||||
|
#![cfg(test)]
|
||||||
|
// 裁决方授权:扩展认证沿用核心套件的纯风格类 lint 豁免。安全/正确性
|
||||||
|
// lint 不在豁免范围内。
|
||||||
|
#![allow(clippy::type_complexity)]
|
||||||
|
//! 前置工程 B:历史迁移 fail-closed 扩展认证。
|
||||||
|
//!
|
||||||
|
//! 本文件只扩展、不得替代 `backup_restore_certification.rs`。行为测试必须
|
||||||
|
//! 穿过两个公开入口,证明 v1 旧式 proxy 配置中的损坏不会被默认值修复后
|
||||||
|
//! 静默发布。
|
||||||
|
|
||||||
|
use super::Database;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use rusqlite::{backup::Backup, Connection};
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum RestoreEntry {
|
||||||
|
Sql,
|
||||||
|
Binary,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum V1Damage {
|
||||||
|
None,
|
||||||
|
MissingPrimaryRow,
|
||||||
|
WrongStorageClass,
|
||||||
|
FieldDecodeFailure,
|
||||||
|
MissingCircuitRow,
|
||||||
|
MissingSettingRow,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestHomeGuard(Option<std::ffi::OsString>);
|
||||||
|
|
||||||
|
impl TestHomeGuard {
|
||||||
|
fn set(path: &Path) -> Self {
|
||||||
|
let previous = std::env::var_os("CC_SWITCH_TEST_HOME");
|
||||||
|
std::env::set_var("CC_SWITCH_TEST_HOME", path);
|
||||||
|
Self(previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestHomeGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
match self.0.take() {
|
||||||
|
Some(previous) => std::env::set_var("CC_SWITCH_TEST_HOME", previous),
|
||||||
|
None => std::env::remove_var("CC_SWITCH_TEST_HOME"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v1_proxy_fixture(damage: V1Damage) -> String {
|
||||||
|
let proxy_row = match damage {
|
||||||
|
V1Damage::MissingPrimaryRow => "(2, 0, '127.0.0.1', 15721, 1, 3, 60, 120, 600)",
|
||||||
|
V1Damage::WrongStorageClass => "(1, 0, '127.0.0.1', X'00', 1, 3, 60, 120, 600)",
|
||||||
|
V1Damage::FieldDecodeFailure => "(1, 0, '127.0.0.1', 15721, 1, 3, 2147483648, 120, 600)",
|
||||||
|
V1Damage::None | V1Damage::MissingCircuitRow | V1Damage::MissingSettingRow => {
|
||||||
|
"(1, 0, '127.0.0.1', 15721, 1, 3, 60, 120, 600)"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let circuit_row = if matches!(damage, V1Damage::MissingCircuitRow) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"INSERT INTO circuit_breaker_config VALUES (1, 5, 2, 60, 0.5, 10);"
|
||||||
|
};
|
||||||
|
let settings_rows = if matches!(damage, V1Damage::MissingSettingRow) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"INSERT INTO settings (key, value) VALUES
|
||||||
|
('proxy_takeover_claude', 'false'),
|
||||||
|
('auto_failover_enabled_claude', 'false'),
|
||||||
|
('proxy_takeover_codex', 'false'),
|
||||||
|
('auto_failover_enabled_codex', 'false'),
|
||||||
|
('proxy_takeover_gemini', 'false'),
|
||||||
|
('auto_failover_enabled_gemini', 'false');"
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"-- CC Switch SQLite 导出
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
CREATE TABLE proxy_config (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
proxy_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
listen_address TEXT NOT NULL,
|
||||||
|
listen_port INTEGER NOT NULL,
|
||||||
|
enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||||
|
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||||
|
streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
|
||||||
|
streaming_idle_timeout INTEGER NOT NULL DEFAULT 120,
|
||||||
|
non_streaming_timeout INTEGER NOT NULL DEFAULT 600
|
||||||
|
);
|
||||||
|
INSERT INTO proxy_config (
|
||||||
|
id, proxy_enabled, listen_address, listen_port, enable_logging,
|
||||||
|
max_retries, streaming_first_byte_timeout, streaming_idle_timeout,
|
||||||
|
non_streaming_timeout
|
||||||
|
) VALUES {proxy_row};
|
||||||
|
CREATE TABLE circuit_breaker_config (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
failure_threshold INTEGER NOT NULL,
|
||||||
|
success_threshold INTEGER NOT NULL,
|
||||||
|
timeout_seconds INTEGER NOT NULL,
|
||||||
|
error_rate_threshold REAL NOT NULL,
|
||||||
|
min_requests INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
{circuit_row}
|
||||||
|
CREATE TABLE settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT
|
||||||
|
);
|
||||||
|
{settings_rows}
|
||||||
|
PRAGMA user_version = 1;
|
||||||
|
COMMIT;"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_binary_fixture(sql: &str, path: &Path) -> Result<(), AppError> {
|
||||||
|
let source = Connection::open_in_memory()?;
|
||||||
|
source.execute_batch(sql)?;
|
||||||
|
let mut destination = Connection::open(path)?;
|
||||||
|
{
|
||||||
|
let backup = Backup::new(&source, &mut destination)?;
|
||||||
|
backup.run_to_completion(64, Duration::ZERO, None)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_restore_fixture(
|
||||||
|
target: &Database,
|
||||||
|
fixture: &str,
|
||||||
|
entry: RestoreEntry,
|
||||||
|
filename: &str,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
match entry {
|
||||||
|
RestoreEntry::Sql => target.import_sql_string(fixture),
|
||||||
|
RestoreEntry::Binary => {
|
||||||
|
let backup_dir = crate::config::get_app_config_dir().join("backups");
|
||||||
|
std::fs::create_dir_all(&backup_dir)
|
||||||
|
.map_err(|error| AppError::io(&backup_dir, error))?;
|
||||||
|
write_binary_fixture(fixture, &backup_dir.join(filename))?;
|
||||||
|
target.restore_from_backup(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seed_live_sentinel(target: &Database) -> Result<(), AppError> {
|
||||||
|
let conn = super::lock_conn!(target.conn);
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO providers (
|
||||||
|
id, app_type, name, settings_config, meta, is_current
|
||||||
|
) VALUES (
|
||||||
|
'migration-cert-live', 'claude', 'live sentinel', '{}', '{}', 0
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_live_sentinel_unchanged(target: &Database) -> Result<(), AppError> {
|
||||||
|
let conn = super::lock_conn!(target.conn);
|
||||||
|
let count: i64 = conn.query_row(
|
||||||
|
"SELECT COUNT(*) FROM providers
|
||||||
|
WHERE id = 'migration-cert-live'
|
||||||
|
AND app_type = 'claude'
|
||||||
|
AND name = 'live sentinel'",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
count, 1,
|
||||||
|
"failed restore must leave the live database intact"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_schema_fixture(
|
||||||
|
version: i32,
|
||||||
|
mutate: impl FnOnce(&Connection) -> Result<(), AppError>,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
let source = Database::memory()?;
|
||||||
|
{
|
||||||
|
let conn = super::lock_conn!(source.conn);
|
||||||
|
mutate(&conn)?;
|
||||||
|
Database::set_user_version(&conn, version)?;
|
||||||
|
}
|
||||||
|
source.export_sql_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_fixture_rejected(
|
||||||
|
label: &str,
|
||||||
|
expected_step: &str,
|
||||||
|
fixture: &str,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
for entry in [RestoreEntry::Sql, RestoreEntry::Binary] {
|
||||||
|
let target = Database::memory()?;
|
||||||
|
seed_live_sentinel(&target)?;
|
||||||
|
let filename = format!("{label}-{entry:?}.db").to_ascii_lowercase();
|
||||||
|
let error = match run_restore_fixture(&target, fixture, entry, &filename) {
|
||||||
|
Ok(value) => {
|
||||||
|
panic!("{label} via {entry:?} silently survived repair-requiring input: {value:?}")
|
||||||
|
}
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
matches!(error, AppError::InvalidInput(_)),
|
||||||
|
"{label} via {entry:?} must return structured InvalidInput, got {error:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error.to_string().contains(expected_step),
|
||||||
|
"{label} via {entry:?} failed at the wrong boundary: {error}"
|
||||||
|
);
|
||||||
|
assert_live_sentinel_unchanged(&target)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn v1_damaged_proxy_rows_fail_closed_through_sql_and_binary_entries() -> Result<(), AppError> {
|
||||||
|
let home = tempfile::tempdir().map_err(|error| AppError::IoContext {
|
||||||
|
context: "create migration certification home".to_string(),
|
||||||
|
source: error,
|
||||||
|
})?;
|
||||||
|
let _home_guard = TestHomeGuard::set(home.path());
|
||||||
|
|
||||||
|
for damage in [
|
||||||
|
V1Damage::MissingPrimaryRow,
|
||||||
|
V1Damage::WrongStorageClass,
|
||||||
|
V1Damage::FieldDecodeFailure,
|
||||||
|
V1Damage::MissingCircuitRow,
|
||||||
|
V1Damage::MissingSettingRow,
|
||||||
|
] {
|
||||||
|
for entry in [RestoreEntry::Sql, RestoreEntry::Binary] {
|
||||||
|
let target = Database::memory()?;
|
||||||
|
seed_live_sentinel(&target)?;
|
||||||
|
let fixture = v1_proxy_fixture(damage);
|
||||||
|
let filename = format!("v1-{damage:?}-{entry:?}.db").to_ascii_lowercase();
|
||||||
|
|
||||||
|
let error = run_restore_fixture(&target, &fixture, entry, &filename)
|
||||||
|
.expect_err("damaged v1 restore must fail closed");
|
||||||
|
assert!(
|
||||||
|
matches!(error, AppError::InvalidInput(_)),
|
||||||
|
"migration damage must be a structured InvalidInput, got {error:?}"
|
||||||
|
);
|
||||||
|
assert_live_sentinel_unchanged(&target)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn valid_v1_proxy_rows_migrate_losslessly_through_sql_and_binary_entries() -> Result<(), AppError> {
|
||||||
|
let home = tempfile::tempdir().map_err(|error| AppError::IoContext {
|
||||||
|
context: "create valid v1 migration certification home".to_string(),
|
||||||
|
source: error,
|
||||||
|
})?;
|
||||||
|
let _home_guard = TestHomeGuard::set(home.path());
|
||||||
|
let fixture = v1_proxy_fixture(V1Damage::None);
|
||||||
|
|
||||||
|
for entry in [RestoreEntry::Sql, RestoreEntry::Binary] {
|
||||||
|
let target = Database::memory()?;
|
||||||
|
let filename = format!("valid-v1-{entry:?}.db").to_ascii_lowercase();
|
||||||
|
run_restore_fixture(&target, &fixture, entry, &filename)?;
|
||||||
|
let conn = super::lock_conn!(target.conn);
|
||||||
|
let restored: (i64, String, i64) = conn.query_row(
|
||||||
|
"SELECT
|
||||||
|
COUNT(*),
|
||||||
|
MIN(listen_address),
|
||||||
|
MIN(listen_port)
|
||||||
|
FROM proxy_config",
|
||||||
|
[],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
restored,
|
||||||
|
(4, "127.0.0.1".to_string(), 15721),
|
||||||
|
"valid authoritative v1 fields must survive {entry:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn every_repair_class_has_a_public_entry_fail_closed_sentinel() -> Result<(), AppError> {
|
||||||
|
let home = tempfile::tempdir().map_err(|error| AppError::IoContext {
|
||||||
|
context: "create migration repair certification home".to_string(),
|
||||||
|
source: error,
|
||||||
|
})?;
|
||||||
|
let _home_guard = TestHomeGuard::set(home.path());
|
||||||
|
|
||||||
|
let fixtures = vec![
|
||||||
|
(
|
||||||
|
"v1-unprefixed-skill",
|
||||||
|
"v1->v2 skills",
|
||||||
|
current_schema_fixture(1, |conn| {
|
||||||
|
conn.execute_batch(
|
||||||
|
"DROP TABLE skills;
|
||||||
|
CREATE TABLE skills (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
installed BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
installed_at INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
INSERT INTO skills (key, installed, installed_at)
|
||||||
|
VALUES ('missing-prefix', 1, 10);",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v2-nonempty-skills",
|
||||||
|
"v2->v3 skills SSOT rebuild",
|
||||||
|
current_schema_fixture(2, |conn| {
|
||||||
|
conn.execute_batch(
|
||||||
|
"DROP TABLE skills;
|
||||||
|
CREATE TABLE skills (
|
||||||
|
directory TEXT NOT NULL,
|
||||||
|
app_type TEXT NOT NULL,
|
||||||
|
installed BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
installed_at INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (directory, app_type)
|
||||||
|
);
|
||||||
|
INSERT INTO skills
|
||||||
|
(directory, app_type, installed, installed_at)
|
||||||
|
VALUES ('legacy-skill', 'claude', 1, 10);",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v5-invalid-provider-meta",
|
||||||
|
"v5->v6 provider meta",
|
||||||
|
current_schema_fixture(5, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO providers
|
||||||
|
(id, app_type, name, settings_config, meta)
|
||||||
|
VALUES ('bad-meta', 'claude', 'bad meta', '{}', '{')",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v5-provider-meta-normalization",
|
||||||
|
"v5->v6 provider meta",
|
||||||
|
current_schema_fixture(5, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO providers
|
||||||
|
(id, app_type, name, settings_config, meta)
|
||||||
|
VALUES (
|
||||||
|
'copilot-meta', 'claude', 'copilot meta', '{}',
|
||||||
|
'{\"usage_script\":{\"template_type\":\"copilot\"}}'
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v7-pricing-correction",
|
||||||
|
"v7->v8 model pricing correction",
|
||||||
|
current_schema_fixture(7, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE model_pricing
|
||||||
|
SET input_cost_per_million = '999'
|
||||||
|
WHERE model_id = 'deepseek-v3.2'",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v8-pricing-reseed",
|
||||||
|
"v8->v9 model-pricing replacement",
|
||||||
|
current_schema_fixture(8, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO model_pricing (
|
||||||
|
model_id, display_name, input_cost_per_million,
|
||||||
|
output_cost_per_million, cache_read_cost_per_million,
|
||||||
|
cache_creation_cost_per_million
|
||||||
|
) VALUES ('untrusted-extra', 'extra', '1', '1', '0', '0')",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v10-rollup-synthesis",
|
||||||
|
"v10->v11 usage rollup rebuild",
|
||||||
|
current_schema_fixture(10, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO usage_daily_rollups
|
||||||
|
(date, app_type, provider_id, model)
|
||||||
|
VALUES ('2026-08-01', 'claude', 'legacy', 'legacy-model')",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v13-grokbuild-synthesis",
|
||||||
|
"v13->v14 proxy rebuild",
|
||||||
|
current_schema_fixture(13, |conn| {
|
||||||
|
conn.execute("DELETE FROM proxy_config WHERE app_type = 'grokbuild'", [])?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v13-missing-column-fallback",
|
||||||
|
"v13->v14 proxy rebuild",
|
||||||
|
current_schema_fixture(13, |conn| {
|
||||||
|
conn.execute_batch("ALTER TABLE proxy_config DROP COLUMN live_takeover_active;")?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v13-wrong-storage",
|
||||||
|
"v13->v14 proxy rebuild",
|
||||||
|
current_schema_fixture(13, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE proxy_config SET listen_port = X'00'
|
||||||
|
WHERE app_type = 'claude'",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v15-codex-log-reset",
|
||||||
|
"v15->v16 Codex usage reset",
|
||||||
|
current_schema_fixture(15, |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO proxy_request_logs (
|
||||||
|
request_id, provider_id, app_type, model,
|
||||||
|
latency_ms, status_code, created_at, data_source
|
||||||
|
) VALUES (
|
||||||
|
'untrusted-codex-log', '_codex_session', 'codex', 'legacy',
|
||||||
|
1, 200, 1, 'codex_session'
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (label, expected_step, fixture) in fixtures {
|
||||||
|
assert_fixture_rejected(label, expected_step, &fixture)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migration_classification_and_no_swallowing_guard_cover_v1_through_v17() {
|
||||||
|
let source = include_str!("schema.rs");
|
||||||
|
for from in 1..17 {
|
||||||
|
let to = from + 1;
|
||||||
|
let classification_row = format!("| v{from}→v{to}");
|
||||||
|
assert!(
|
||||||
|
source.contains(&classification_row),
|
||||||
|
"migration classification table is missing v{from}->v{to}"
|
||||||
|
);
|
||||||
|
let dispatch = format!("Self::migrate_v{from}_to_v{to}(conn, context)?;");
|
||||||
|
assert!(
|
||||||
|
source.contains(&dispatch),
|
||||||
|
"v{from}->v{to} must receive MigrationRunContext at dispatch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let migration_start = source
|
||||||
|
.find("fn migrate_v1_to_v2")
|
||||||
|
.expect("v1 migration exists");
|
||||||
|
let migration_end = source
|
||||||
|
.find("/// 插入默认模型定价数据")
|
||||||
|
.expect("migration span end exists");
|
||||||
|
let migration_span = &source[migration_start..migration_end];
|
||||||
|
for forbidden in [
|
||||||
|
".unwrap_or(",
|
||||||
|
".unwrap_or_else(",
|
||||||
|
".unwrap_or_default(",
|
||||||
|
".ok()",
|
||||||
|
"if let Ok(",
|
||||||
|
"let _ =",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!migration_span.contains(forbidden),
|
||||||
|
"historical migration span contains forbidden error swallowing: {forbidden}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
source.contains("untrusted schema migration v{version}->v{} failed"),
|
||||||
|
"the migration boundary must map non-structured failures to InvalidInput"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
pub(crate) mod backup;
|
pub(crate) mod backup;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod backup_restore_certification;
|
mod backup_restore_certification;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod backup_restore_certification_ext;
|
||||||
mod dao;
|
mod dao;
|
||||||
mod migration;
|
mod migration;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|||||||
+634
-142
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user