mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
fix(database): enforce canonical restore trust boundaries
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
#![cfg(test)]
|
||||
//! 前置工程 B:Canonical Restore 认证测试套件 v1(测试先行)
|
||||
// 裁决方授权:认证文件允许豁免纯风格类 clippy lint(不含任何安全/正确性
|
||||
// lint)。type_complexity 属阈值型风格检查,对测试专用契约文件无意义。
|
||||
#![allow(clippy::type_complexity)]
|
||||
//! 前置工程 B:Canonical Restore 认证测试套件 v2(测试先行)
|
||||
//!
|
||||
//! 规则与前置 A 完全一致(docs/pi-support-restructure-zh.md):实现方不得
|
||||
//! 修改本文件;异议上报裁决;全绿是盲审前置条件而非充分条件。
|
||||
@@ -29,13 +32,44 @@
|
||||
//! 路径任何组件在检查后不得再经 symlink 重解析。**盲审重点核查项**。
|
||||
//! O3 pre-B 认证通过后,裁决方重冻 infra 基线并将本套件红灯清单归零存档。
|
||||
//!
|
||||
//! ## 交接时的预期红绿
|
||||
//! 应红 2:`certify_imported_sort_index_domain_is_enforced`(值域缺口:
|
||||
//! RestoreColumnSpec 只查 storage/nullable,负数 sort_index 可发布,生产端
|
||||
//! Option<usize> 读取即败——R4 finding)、
|
||||
//! `certify_incremental_auto_vacuum_survives_restore`(canonical stage 未
|
||||
//! 继承 INCREMENTAL,publish 后 live 文件退化为 NONE——R4 finding)。
|
||||
//! 其余应绿。任何偏离(非清单红、应红变绿、编译失败)立即上报。
|
||||
//! ## 首轮认证失败后的契约裁决(v2,2026-08-01)
|
||||
//! 首轮组件盲审(止于 89961dff)确认两 High 两 Medium,均属契约真空,
|
||||
//! 现裁决如下:
|
||||
//! 裁决5【写入所有权边界,High】进程内对 live 数据库文件的**可写连接必须
|
||||
//! 唯一**(AppState Database 主连接);对 live 路径的任何其他打开必须
|
||||
//! 携带 SQLITE_OPEN_READ_ONLY(database/mod.rs 的版本预检连接是现存违
|
||||
//! 例)。restore 在 Database mutex 内自 safety backup 前持锁至 publish
|
||||
//! 完成——唯一可写连接使该 mutex 成为真实的全局写边界,堵死"第二连接
|
||||
//! 在 safety backup 后提交、被 publish 覆盖"的窗口。结构红灯 R5;
|
||||
//! **盲审重点**:全树审计 Connection::open* 调用点与其目标路径。
|
||||
//! 裁决6【迁移语义分离,High】迁移链必须携带
|
||||
//! `MigrationRunContext { LocalUpgrade, UntrustedRestore }`;一切修复/
|
||||
//! 合并/去重步骤(含 v16→v17 的 endpoint 去重)在 UntrustedRestore 下
|
||||
//! 必须整体 abort 并给结构化错误,LocalUpgrade 保留既有容错。结构红灯
|
||||
//! R6(调用点级绑定:enum 形状/迁移入口签名/不可信入口传
|
||||
//! UntrustedRestore/本地入口传 LocalUpgrade);行为义务 O4:用既有
|
||||
//! user_version 哨兵机器构造 v16 重复 endpoint 输入,断言 import 整体
|
||||
//! Err、零合并——**O4 交付并经裁决并入是首轮重认证的前置条件**,
|
||||
//! 不得以结构绿替代语义覆盖。语义注记:该枚举表达"修复策略",
|
||||
//! fresh/canonical bootstrap 映射 LocalUpgrade。
|
||||
//! 裁决7【REAL 值域,Medium】浮点域列必须声明有限区间;
|
||||
//! `circuit_error_rate_threshold` ∈ [0,1] 且有限。行为红灯 R7。
|
||||
//! O2 扩展【Windows TOCTOU,Medium】Windows 二进制路径必须基于已验证目录
|
||||
//! 句柄相对打开(参考 NtCreateFile RootDirectory 语义)或在打开后按句柄
|
||||
//! 身份复核,且补文件级预检;**盲审重点**,Linux 宿主无法执行,逐行核查。
|
||||
//! 裁决5 边界补记:重复 Database::init()、其他连接 ATTACH live 库均属
|
||||
//! 进程内旁路,纳入全树审计(盲审);**进程外写者**(第二进程、外部
|
||||
//! SQLite 客户端、直接替换文件)不受进程内边界约束,single-instance
|
||||
//! 插件不是数据库锁——显式列为残余风险,由发布说明与盲审知悉。
|
||||
//! 重认证预算:修复后 2 轮 fresh 双审;写入所有权或迁移语义同 invariant
|
||||
//! 复发 → 停止并上报用户。
|
||||
//!
|
||||
//! ## 当前预期红绿(v2,基于 89961dff 之后的工作树)
|
||||
//! 首轮实现已使 R1(sort_index 值域)与 R2(auto_vacuum)转绿。当前应红 3:
|
||||
//! `certify_live_database_secondary_opens_are_read_only`(裁决5)、
|
||||
//! `certify_untrusted_migration_context_is_threaded`(裁决6)、
|
||||
//! `certify_imported_circuit_threshold_domain_is_enforced`(裁决7)。
|
||||
//! 其余 7 项应绿。任何偏离(非清单红、应红变绿、编译失败)立即上报。
|
||||
//!
|
||||
//! ## 残余风险与收口
|
||||
//! 沿用前置 A 的收口边界与残余清单(动态 SQL、trigger/view、Backup API、
|
||||
@@ -607,3 +641,447 @@ fn certify_incremental_auto_vacuum_survives_restore() {
|
||||
(canonical stage must inherit INCREMENTAL before its first page is written)"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// R5(应红,裁决5):live 库的次级连接必须只读——调用级检查
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 每个函数收集连接打开调用:(调用名, 该调用实参中的 ident 集)。
|
||||
#[derive(Default)]
|
||||
struct OpenCallCollector {
|
||||
calls: Vec<(String, BTreeSet<String>)>,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for OpenCallCollector {
|
||||
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
|
||||
if let syn::Expr::Path(path) = node.func.as_ref() {
|
||||
if let Some(segment) = path.path.segments.last() {
|
||||
let name = segment.ident.to_string();
|
||||
if matches!(name.as_str(), "open" | "open_with_flags" | "open_in_memory") {
|
||||
let mut args = IdentProbe::default();
|
||||
for arg in &node.args {
|
||||
args.visit_expr(arg);
|
||||
}
|
||||
self.calls.push((name, args.found));
|
||||
}
|
||||
}
|
||||
}
|
||||
visit::visit_expr_call(self, node);
|
||||
}
|
||||
|
||||
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
|
||||
let name = node.method.to_string();
|
||||
if matches!(name.as_str(), "open" | "open_with_flags" | "open_in_memory") {
|
||||
let mut args = IdentProbe::default();
|
||||
for arg in &node.args {
|
||||
args.visit_expr(arg);
|
||||
}
|
||||
self.calls.push((name, args.found));
|
||||
}
|
||||
visit::visit_expr_method_call(self, node);
|
||||
}
|
||||
}
|
||||
|
||||
/// 裁决5 的调用级规则。返回违规描述;空即合规。
|
||||
fn live_open_violations(fn_name: &str, calls: &[(String, BTreeSet<String>)]) -> Vec<String> {
|
||||
let mut violations = Vec::new();
|
||||
let mut primary_opens = 0usize;
|
||||
for (call, args) in calls {
|
||||
match call.as_str() {
|
||||
"open_in_memory" => {}
|
||||
"open" => {
|
||||
if fn_name == "init" {
|
||||
primary_opens += 1;
|
||||
} else {
|
||||
violations.push(format!(
|
||||
"{fn_name}: bare Connection::open is read-write; secondary \
|
||||
connections must use open_with_flags + SQLITE_OPEN_READ_ONLY"
|
||||
));
|
||||
}
|
||||
}
|
||||
"open_with_flags" => {
|
||||
if fn_name == "init" {
|
||||
primary_opens += 1;
|
||||
} else if !args.contains("SQLITE_OPEN_READ_ONLY") {
|
||||
violations.push(format!(
|
||||
"{fn_name}: open_with_flags without SQLITE_OPEN_READ_ONLY"
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if fn_name == "init" && primary_opens > 1 {
|
||||
violations.push(
|
||||
"init: more than one primary open; the writable live connection must be unique"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
violations
|
||||
}
|
||||
|
||||
fn collect_open_calls(items: &[Item], out: &mut Vec<(String, Vec<(String, BTreeSet<String>)>)>) {
|
||||
for item in items {
|
||||
match item {
|
||||
Item::Fn(function) if !attrs_mark_test_only(&function.attrs) => {
|
||||
let mut collector = OpenCallCollector::default();
|
||||
collector.visit_block(&function.block);
|
||||
out.push((function.sig.ident.to_string(), collector.calls));
|
||||
}
|
||||
Item::Impl(item_impl) if !attrs_mark_test_only(&item_impl.attrs) => {
|
||||
for impl_item in &item_impl.items {
|
||||
let ImplItem::Fn(function) = impl_item else {
|
||||
continue;
|
||||
};
|
||||
if !attrs_mark_test_only(&function.attrs) {
|
||||
let mut collector = OpenCallCollector::default();
|
||||
collector.visit_block(&function.block);
|
||||
out.push((function.sig.ident.to_string(), collector.calls));
|
||||
}
|
||||
}
|
||||
}
|
||||
Item::Mod(item_mod) if !attrs_mark_test_only(&item_mod.attrs) => {
|
||||
if let Some((_, nested)) = &item_mod.content {
|
||||
collect_open_calls(nested, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certify_live_database_secondary_opens_are_read_only() {
|
||||
// 负向 fixture 先证明检查器会响(修复后假绿是 R9 终审确认的 P0)。
|
||||
let fixture = r#"
|
||||
impl Database {
|
||||
fn bad_bare(path: &Path) { let _ = Connection::open(path); }
|
||||
fn bad_flags(path: &Path) {
|
||||
let _ = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_WRITE);
|
||||
}
|
||||
fn good_flags(path: &Path) {
|
||||
let _ = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY);
|
||||
}
|
||||
fn good_memory() { let _ = Connection::open_in_memory(); }
|
||||
fn init(path: &Path) {
|
||||
let _ = Connection::open(path);
|
||||
let _ = Connection::open(path);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let parsed = syn::parse_file(fixture).expect("parse fixture");
|
||||
let mut fixture_fns = Vec::new();
|
||||
collect_open_calls(&parsed.items, &mut fixture_fns);
|
||||
let violations_of = |name: &str, fns: &[(String, Vec<(String, BTreeSet<String>)>)]| {
|
||||
fns.iter()
|
||||
.filter(|(fn_name, _)| fn_name == name)
|
||||
.flat_map(|(fn_name, calls)| live_open_violations(fn_name, calls))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert!(!violations_of("bad_bare", &fixture_fns).is_empty());
|
||||
assert!(!violations_of("bad_flags", &fixture_fns).is_empty());
|
||||
assert!(violations_of("good_flags", &fixture_fns).is_empty());
|
||||
assert!(violations_of("good_memory", &fixture_fns).is_empty());
|
||||
assert!(
|
||||
!violations_of("init", &fixture_fns).is_empty(),
|
||||
"a second primary open inside init must be flagged"
|
||||
);
|
||||
|
||||
// 真实检查:database/mod.rs 全部生产函数。
|
||||
let source =
|
||||
fs::read_to_string(source_root().join("database/mod.rs")).expect("read database/mod.rs");
|
||||
let syntax = syn::parse_file(&source).expect("parse database/mod.rs");
|
||||
let mut fns = Vec::new();
|
||||
collect_open_calls(&syntax.items, &mut fns);
|
||||
let mut violations = Vec::new();
|
||||
for (name, calls) in &fns {
|
||||
if name == "memory" {
|
||||
continue; // 测试内存库工厂
|
||||
}
|
||||
violations.extend(live_open_violations(name, calls));
|
||||
}
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"writable live-database connections must be unique to init: {violations:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// R6(应红,裁决6):迁移语义分离——迁移调用实参级绑定
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 收集对 `apply_schema_migrations_on_conn` 的每次调用及其实参 ident 集。
|
||||
#[derive(Default)]
|
||||
struct MigrationCallCollector {
|
||||
calls: Vec<BTreeSet<String>>,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for MigrationCallCollector {
|
||||
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
|
||||
if let syn::Expr::Path(path) = node.func.as_ref() {
|
||||
if path
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|segment| segment.ident == "apply_schema_migrations_on_conn")
|
||||
{
|
||||
let mut args = IdentProbe::default();
|
||||
for arg in &node.args {
|
||||
args.visit_expr(arg);
|
||||
}
|
||||
self.calls.push(args.found);
|
||||
}
|
||||
}
|
||||
visit::visit_expr_call(self, node);
|
||||
}
|
||||
|
||||
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
|
||||
if node.method == "apply_schema_migrations_on_conn" {
|
||||
let mut args = IdentProbe::default();
|
||||
for arg in &node.args {
|
||||
args.visit_expr(arg);
|
||||
}
|
||||
self.calls.push(args.found);
|
||||
}
|
||||
visit::visit_expr_method_call(self, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn certify_untrusted_migration_context_is_threaded() {
|
||||
// 函数体 ident 聚合会被迁移函数内部的 `match context` 分支假绿
|
||||
// (R10 终审 P0),因此绑定到调用实参:
|
||||
// 1) enum 精确形状;2) 确切入口 apply_schema_migrations_on_conn 的签名
|
||||
// 携带 context;3) 不可信区域的迁移调用实参含 UntrustedRestore 且绝无
|
||||
// LocalUpgrade;4) 本地区域的迁移调用实参含 LocalUpgrade 且绝无
|
||||
// UntrustedRestore。v16 去重 fail-closed 的行为覆盖由义务 O4 交付
|
||||
// (首轮重认证前置条件)。语义注记:枚举表达"修复策略",
|
||||
// fresh/canonical bootstrap 映射 LocalUpgrade。
|
||||
let schema = syn::parse_file(&schema_source()).expect("parse schema.rs");
|
||||
let migration_source =
|
||||
fs::read_to_string(source_root().join("database/migration.rs")).expect("read migration.rs");
|
||||
let migration = syn::parse_file(&migration_source).expect("parse migration.rs");
|
||||
|
||||
// 1) enum 精确形状
|
||||
fn find_enum<'a>(items: &'a [Item], name: &str) -> Option<&'a syn::ItemEnum> {
|
||||
for item in items {
|
||||
match item {
|
||||
Item::Enum(item_enum) if item_enum.ident == name => return Some(item_enum),
|
||||
Item::Mod(item_mod) => {
|
||||
let nested = item_mod.content.as_ref().map(|(_, items)| items.as_slice());
|
||||
if let Some(found) = nested.and_then(|items| find_enum(items, name)) {
|
||||
return Some(found);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
let context_enum = find_enum(&schema.items, "MigrationRunContext")
|
||||
.or_else(|| find_enum(&migration.items, "MigrationRunContext"))
|
||||
.expect("MigrationRunContext enum must exist in the migration chain");
|
||||
let variants: Vec<String> = context_enum
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| variant.ident.to_string())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
variants,
|
||||
vec!["LocalUpgrade".to_string(), "UntrustedRestore".to_string()],
|
||||
"MigrationRunContext variants drifted from the adjudicated contract"
|
||||
);
|
||||
|
||||
// 2) 确切入口签名携带 context
|
||||
fn entry_takes_context(items: &[Item]) -> bool {
|
||||
for item in items {
|
||||
match item {
|
||||
Item::Fn(function) if function.sig.ident == "apply_schema_migrations_on_conn" => {
|
||||
let mut probe = IdentProbe::default();
|
||||
for input in &function.sig.inputs {
|
||||
if let syn::FnArg::Typed(typed) = input {
|
||||
probe.visit_type(&typed.ty);
|
||||
}
|
||||
}
|
||||
return probe.found.contains("MigrationRunContext");
|
||||
}
|
||||
Item::Impl(item_impl) => {
|
||||
for impl_item in &item_impl.items {
|
||||
let ImplItem::Fn(function) = impl_item else {
|
||||
continue;
|
||||
};
|
||||
if function.sig.ident != "apply_schema_migrations_on_conn" {
|
||||
continue;
|
||||
}
|
||||
let mut probe = IdentProbe::default();
|
||||
for input in &function.sig.inputs {
|
||||
if let syn::FnArg::Typed(typed) = input {
|
||||
probe.visit_type(&typed.ty);
|
||||
}
|
||||
}
|
||||
return probe.found.contains("MigrationRunContext");
|
||||
}
|
||||
}
|
||||
Item::Mod(item_mod) => {
|
||||
if let Some((_, nested)) = &item_mod.content {
|
||||
if entry_takes_context(nested) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
assert!(
|
||||
entry_takes_context(&schema.items) || entry_takes_context(&migration.items),
|
||||
"apply_schema_migrations_on_conn must take MigrationRunContext in its signature"
|
||||
);
|
||||
|
||||
// 3) 不可信区域:UntrustedScratch impl 内的迁移调用
|
||||
let backup = syn::parse_file(&backup_source()).expect("parse backup.rs");
|
||||
let mut untrusted_calls = MigrationCallCollector::default();
|
||||
for item in &backup.items {
|
||||
let Item::Impl(item_impl) = item else {
|
||||
continue;
|
||||
};
|
||||
let matches_type = matches!(
|
||||
item_impl.self_ty.as_ref(),
|
||||
syn::Type::Path(type_path)
|
||||
if type_path
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|segment| segment.ident == "UntrustedScratch")
|
||||
);
|
||||
if !matches_type {
|
||||
continue;
|
||||
}
|
||||
for impl_item in &item_impl.items {
|
||||
if let ImplItem::Fn(function) = impl_item {
|
||||
untrusted_calls.visit_block(&function.block);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
untrusted_calls
|
||||
.calls
|
||||
.iter()
|
||||
.any(|args| args.contains("UntrustedRestore")),
|
||||
"the untrusted scratch pipeline must call migrations with UntrustedRestore"
|
||||
);
|
||||
assert!(
|
||||
untrusted_calls
|
||||
.calls
|
||||
.iter()
|
||||
.all(|args| !args.contains("LocalUpgrade")),
|
||||
"the untrusted pipeline must never request tolerant LocalUpgrade repairs"
|
||||
);
|
||||
|
||||
// 4) 本地区域:database/mod.rs ∪ schema.rs ∪ migration.rs 生产函数内的
|
||||
// 迁移调用(排除 backup.rs;调用实参而非函数体,match 分支无法假绿)
|
||||
let mod_source =
|
||||
fs::read_to_string(source_root().join("database/mod.rs")).expect("read database/mod.rs");
|
||||
let mod_parsed = syn::parse_file(&mod_source).expect("parse database/mod.rs");
|
||||
let mut local_calls = MigrationCallCollector::default();
|
||||
fn visit_production_fns(items: &[Item], collector: &mut MigrationCallCollector) {
|
||||
for item in items {
|
||||
match item {
|
||||
Item::Fn(function) if !attrs_mark_test_only(&function.attrs) => {
|
||||
collector.visit_block(&function.block);
|
||||
}
|
||||
Item::Impl(item_impl) if !attrs_mark_test_only(&item_impl.attrs) => {
|
||||
for impl_item in &item_impl.items {
|
||||
if let ImplItem::Fn(function) = impl_item {
|
||||
if !attrs_mark_test_only(&function.attrs) {
|
||||
collector.visit_block(&function.block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item::Mod(item_mod) if !attrs_mark_test_only(&item_mod.attrs) => {
|
||||
if let Some((_, nested)) = &item_mod.content {
|
||||
visit_production_fns(nested, collector);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
visit_production_fns(&mod_parsed.items, &mut local_calls);
|
||||
visit_production_fns(&schema.items, &mut local_calls);
|
||||
visit_production_fns(&migration.items, &mut local_calls);
|
||||
assert!(
|
||||
local_calls
|
||||
.calls
|
||||
.iter()
|
||||
.any(|args| args.contains("LocalUpgrade")),
|
||||
"a production local-upgrade call site must pass LocalUpgrade"
|
||||
);
|
||||
assert!(
|
||||
local_calls
|
||||
.calls
|
||||
.iter()
|
||||
.all(|args| !args.contains("UntrustedRestore")),
|
||||
"local upgrade paths must never run under UntrustedRestore semantics"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// R7(应红,裁决7):REAL 值域——熔断阈值必须限于有限 [0,1](定向篡改)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn certify_imported_circuit_threshold_domain_is_enforced() {
|
||||
let database = Database::memory().expect("memory db");
|
||||
let exported = database.export_sql_string().expect("export");
|
||||
// 定向替换:只动 proxy_config 表插入行里的阈值,不碰 DDL 默认值与
|
||||
// model_pricing 的 0.60/0.65 子串(R9 终审确认全局替换会误伤)。
|
||||
for hostile_value in ["6.5", "-0.5", "1e999"] {
|
||||
let mut replaced = 0usize;
|
||||
let hostile: String = exported
|
||||
.lines()
|
||||
.map(|line| {
|
||||
if replaced == 0
|
||||
&& line.contains("proxy_config")
|
||||
&& !line.to_ascii_uppercase().contains("CREATE TABLE")
|
||||
&& line.contains(", 0.6,")
|
||||
{
|
||||
replaced += 1;
|
||||
let mut tampered = line.replacen(", 0.6,", &format!(", {hostile_value},"), 1);
|
||||
tampered.push('\n');
|
||||
tampered
|
||||
} else {
|
||||
let mut kept = line.to_string();
|
||||
kept.push('\n');
|
||||
kept
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
replaced, 1,
|
||||
"exactly one proxy_config insert row must carry the 0.6 sentinel \
|
||||
(export format drifted; escalate for adjudication)"
|
||||
);
|
||||
|
||||
let target = Database::memory().expect("target db");
|
||||
target
|
||||
.import_sql_string(&hostile)
|
||||
.expect_err("circuit_error_rate_threshold outside finite [0,1] must abort the import");
|
||||
let conn = target.conn.lock().expect("lock target");
|
||||
let out_of_domain: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM proxy_config
|
||||
WHERE circuit_error_rate_threshold < 0.0
|
||||
OR circuit_error_rate_threshold > 1.0",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("count out-of-domain thresholds");
|
||||
assert_eq!(
|
||||
out_of_domain, 0,
|
||||
"aborted import must leave no out-of-domain breaker thresholds behind"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user