From 56f7ddd1dc72def3b1ecdfeb6de18e5084b18d34 Mon Sep 17 00:00:00 2001 From: zyycn Date: Sat, 1 Aug 2026 23:28:27 +0800 Subject: [PATCH] test: mirror test directory structure to production modules Reorganize integration tests so tests/ mirrors src/ per architecture rule. - gateway-admin: split flat backup tests into tests/backup/{policy,task,support} and tests/model/backup.rs, sharing fakes via backup::support - gateway-store: move coordination_buffer and execution_buffer tests into tests/redis/ and tests/postgres/ to match src layout Co-Authored-By: Claude Fable 5 --- .../crates/gateway-admin/tests/backup/mod.rs | 5 + .../tests/{backup.rs => backup/policy.rs} | 104 +---------- .../{backup_runtime.rs => backup/support.rs} | 160 +---------------- .../crates/gateway-admin/tests/backup/task.rs | 165 ++++++++++++++++++ backend/crates/gateway-admin/tests/main.rs | 2 +- .../gateway-admin/tests/model/backup.rs | 104 +++++++++++ .../crates/gateway-admin/tests/model/mod.rs | 3 + .../gateway-admin/tests/use_case/backup.rs | 2 +- backend/crates/gateway-store/tests/main.rs | 2 - .../tests/{ => postgres}/execution_buffer.rs | 0 .../gateway-store/tests/postgres/mod.rs | 1 + .../tests/{ => redis}/coordination_buffer.rs | 0 .../crates/gateway-store/tests/redis/mod.rs | 1 + 13 files changed, 289 insertions(+), 260 deletions(-) create mode 100644 backend/crates/gateway-admin/tests/backup/mod.rs rename backend/crates/gateway-admin/tests/{backup.rs => backup/policy.rs} (58%) rename backend/crates/gateway-admin/tests/{backup_runtime.rs => backup/support.rs} (78%) create mode 100644 backend/crates/gateway-admin/tests/backup/task.rs create mode 100644 backend/crates/gateway-admin/tests/model/backup.rs create mode 100644 backend/crates/gateway-admin/tests/model/mod.rs rename backend/crates/gateway-store/tests/{ => postgres}/execution_buffer.rs (100%) rename backend/crates/gateway-store/tests/{ => redis}/coordination_buffer.rs (100%) diff --git a/backend/crates/gateway-admin/tests/backup/mod.rs b/backend/crates/gateway-admin/tests/backup/mod.rs new file mode 100644 index 000000000..da4823694 --- /dev/null +++ b/backend/crates/gateway-admin/tests/backup/mod.rs @@ -0,0 +1,5 @@ +//! 备份模块测试:镜像 src/backup/ 的 policy 与 task。 + +mod policy; +pub mod support; +mod task; diff --git a/backend/crates/gateway-admin/tests/backup.rs b/backend/crates/gateway-admin/tests/backup/policy.rs similarity index 58% rename from backend/crates/gateway-admin/tests/backup.rs rename to backend/crates/gateway-admin/tests/backup/policy.rs index 36c508317..842a511f0 100644 --- a/backend/crates/gateway-admin/tests/backup.rs +++ b/backend/crates/gateway-admin/tests/backup/policy.rs @@ -1,14 +1,10 @@ -//! 备份领域模型与纯策略测试(生产 src 禁止内嵌测试,见 architecture.rs)。 +//! 备份调度与保留策略(backup/policy.rs)的纯逻辑测试。 use chrono::{Duration, TimeZone as _, Utc}; -use secrecy::SecretString; use gateway_admin::{ backup::policy::{BackupSchedule, RetentionReason, decide_retention}, - model::backup::{ - BackupRecord, BackupSettings, BackupStatus, BackupStorageConfig, BackupTriggerKind, - build_object_key, - }, + model::backup::{BackupRecord, BackupStatus, BackupTriggerKind}, }; fn record( @@ -41,102 +37,6 @@ fn record( } } -#[test] -fn status_machine_rejects_invalid_transitions() { - assert!(BackupStatus::Queued.allows_transition_to(BackupStatus::Dumping)); - assert!(BackupStatus::Dumping.allows_transition_to(BackupStatus::Uploading)); - assert!(BackupStatus::Uploading.allows_transition_to(BackupStatus::Completed)); - assert!(BackupStatus::Completed.allows_transition_to(BackupStatus::Deleting)); - assert!(!BackupStatus::Queued.allows_transition_to(BackupStatus::Completed)); - assert!(!BackupStatus::Completed.allows_transition_to(BackupStatus::Dumping)); - assert!(!BackupStatus::Deleting.allows_transition_to(BackupStatus::Failed)); - assert!(!BackupStatus::Deleting.allows_transition_to(BackupStatus::Completed)); -} - -#[test] -fn status_classification() { - assert!(BackupStatus::Queued.is_active()); - assert!(BackupStatus::Uploading.is_active()); - assert!(!BackupStatus::Completed.is_active()); - assert!(BackupStatus::Completed.can_be_deleted()); - assert!(BackupStatus::Failed.can_be_deleted()); - assert!(!BackupStatus::Queued.can_be_deleted()); -} - -#[test] -fn object_key_shape() { - let at = Utc.with_ymd_and_hms(2026, 8, 1, 12, 30, 0).unwrap(); - let key = build_object_key("production/backups", "backup_abc", at).unwrap(); - assert_eq!( - key, - "production/backups/2026/08/01/codex-proxy-rs_20260801_123000_abc.dump" - ); -} - -#[test] -fn object_key_rejects_dangerous_prefixes() { - let at = Utc::now(); - assert!(build_object_key("/leading", "backup_x", at).is_err()); - assert!(build_object_key("a/../b", "backup_x", at).is_err()); - assert!(build_object_key("a\\b", "backup_x", at).is_err()); - assert!(build_object_key("a\u{0001}b", "backup_x", at).is_err()); - assert!(build_object_key("", "backup_x", at).is_err()); - assert!(build_object_key("trailing//", "backup_x", at).is_ok()); -} - -#[test] -fn trigger_and_status_roundtrip() { - for status in [ - BackupStatus::Queued, - BackupStatus::Dumping, - BackupStatus::Uploading, - BackupStatus::Completed, - BackupStatus::Failed, - BackupStatus::Deleting, - ] { - assert_eq!(BackupStatus::parse(status.as_str()), Some(status)); - } - assert_eq!( - BackupTriggerKind::parse("manual"), - Some(BackupTriggerKind::Manual) - ); - assert_eq!( - BackupTriggerKind::parse("scheduled"), - Some(BackupTriggerKind::Scheduled) - ); - assert_eq!(BackupTriggerKind::parse("unknown"), None); - assert_eq!(BackupStatus::parse("skipped"), None); - assert_eq!(BackupStatus::parse("deleted"), None); - assert_eq!(BackupStatus::parse("unknown"), None); -} - -#[test] -fn config_from_settings_redacts_secret_in_debug() { - let mut settings = BackupSettings { - storage_revision: 3, - endpoint: Some("https://example.com".to_owned()), - region: Some("auto".to_owned()), - bucket: Some("b".to_owned()), - access_key_id: Some("ak".to_owned()), - secret_access_key: Some(SecretString::from("sk-secret")), - prefix: Some("p".to_owned()), - force_path_style: false, - schedule_enabled: false, - cron_expression: None, - schedule_timezone: None, - retention_days: 0, - retention_count: 0, - next_run_at: None, - last_verified_at: None, - updated_at: Utc::now(), - }; - assert!(!format!("{:?}", settings).contains("sk-secret")); - let config = BackupStorageConfig::from_settings(&settings).unwrap(); - assert!(!format!("{config:?}").contains("sk-secret")); - settings.endpoint = None; - assert!(BackupStorageConfig::from_settings(&settings).is_none()); -} - #[test] fn parse_rejects_bad_cron_and_timezone() { assert!(BackupSchedule::parse("61 * * * *", "Asia/Shanghai").is_err()); diff --git a/backend/crates/gateway-admin/tests/backup_runtime.rs b/backend/crates/gateway-admin/tests/backup/support.rs similarity index 78% rename from backend/crates/gateway-admin/tests/backup_runtime.rs rename to backend/crates/gateway-admin/tests/backup/support.rs index ea322faf5..1ba63b5c4 100644 --- a/backend/crates/gateway-admin/tests/backup_runtime.rs +++ b/backend/crates/gateway-admin/tests/backup/support.rs @@ -1,14 +1,14 @@ -//! BackupTask 执行链与 BackupService 用例的端口级测试。 +//! 备份测试共享 fixture:内存 fake 端口与辅助函数。 //! -//! 按参考实现 sub2api 的方式:用内存 fake 端口驱动编排层,不依赖 MinIO/真实 S3。 +//! 用内存 fake 端口驱动编排层,不依赖 MinIO/真实 S3。 //! s3.rs / pg_dump.rs 是 SDK 薄适配器,协议正确性交给 aws-sdk / pg_dump 保证。 +//! 供 backup::task 与 use_case::backup 的测试复用。 -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use async_trait::async_trait; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use gateway_admin::{ - backup::task::BackupTask, model::{ MutationActor, MutationContext, Revision, auth::AdminAuditEvent, @@ -26,7 +26,6 @@ use gateway_admin::{ store::{AdminStoreError, AdminStoreErrorKind, AdminStoreResult, AuthStore}, }, }; -use gateway_core::engine::CancellationToken; use secrecy::SecretString; use sha2::{Digest as _, Sha256}; use tempfile::TempDir; @@ -497,7 +496,7 @@ impl DatabaseDumpPort for FakeDumpPort { /// 内存版对象存储:把上传内容按 key 保存并支持 Head/Delete/预签名。 pub(crate) struct FakeObjectStore { - objects: Mutex>>, + pub(crate) objects: Mutex>>, } impl FakeObjectStore { @@ -651,150 +650,3 @@ pub(crate) fn system_context() -> MutationContext { request_id: "backup-runtime-test".to_owned(), } } - -#[tokio::test] -async fn backup_task_runs_full_dump_upload_verify_pipeline() { - let repository = Arc::new(FakeBackupRepository::new(configured_settings())); - let dump = Arc::new(FakeDumpPort::new()); - let object_store = Arc::new(FakeObjectStore::new()); - - let seed = BackupRecordSeed { - id: backup_id("manual"), - trigger_kind: BackupTriggerKind::Manual, - scheduled_at: None, - object_key: "codex/2026/08/01/manual.dump".to_owned(), - expires_at: None, - }; - repository - .insert_backup_record(seed.clone()) - .await - .expect("insert queued"); - - let task = BackupTask::new(repository.clone(), dump.clone(), object_store.clone()); - task.run_cycle(&CancellationToken::new()) - .await - .expect("run cycle"); - - let records = repository.all_records(); - assert_eq!(records.len(), 1); - let finished = &records[0]; - assert_eq!(finished.status, BackupStatus::Completed); - assert_eq!(finished.size_bytes, Some(DUMP_CONTENT.len() as u64)); - assert_eq!( - finished.sha256.as_deref(), - Some(sha256_hex(DUMP_CONTENT).as_str()) - ); - - // 对象已上传且内容与本地归档一致。 - let stored = object_store - .object(&seed.object_key) - .expect("uploaded object exists"); - assert_eq!(stored.as_slice(), DUMP_CONTENT); -} - -#[tokio::test] -async fn backup_task_marks_failed_when_dump_port_fails() { - let repository = Arc::new(FakeBackupRepository::new(configured_settings())); - let dump = Arc::new(FakeDumpPort::new().fail_next_dump()); - let object_store = Arc::new(FakeObjectStore::new()); - - let seed = BackupRecordSeed { - id: backup_id("fail"), - trigger_kind: BackupTriggerKind::Manual, - scheduled_at: None, - object_key: "codex/2026/08/01/fail.dump".to_owned(), - expires_at: None, - }; - repository - .insert_backup_record(seed.clone()) - .await - .expect("insert queued"); - - let task = BackupTask::new(repository.clone(), dump.clone(), object_store.clone()); - task.run_cycle(&CancellationToken::new()) - .await - .expect("run cycle"); - - let records = repository.all_records(); - let failed = &records[0]; - assert_eq!(failed.status, BackupStatus::Failed); - assert_eq!(failed.error_code.as_deref(), Some("backup.pg_dump_failed")); - assert!(failed.completed_at.is_some()); - // 归档未上传,对象不存在。 - assert!(object_store.object(&seed.object_key).is_none()); -} - -#[tokio::test] -async fn retention_cleans_expired_scheduled_backups() { - let repository = Arc::new(FakeBackupRepository::new(configured_settings())); - let dump = Arc::new(FakeDumpPort::new()); - let object_store = Arc::new(FakeObjectStore::new()); - - // 造一条旧 completed 计划备份(超过 retention_days)与一条新备份。 - let old_seed = BackupRecordSeed { - id: backup_id("old"), - trigger_kind: BackupTriggerKind::Scheduled, - scheduled_at: Some(Utc::now() - Duration::days(40)), - object_key: "codex/2026/06/01/old.dump".to_owned(), - expires_at: None, - }; - let new_seed = BackupRecordSeed { - id: backup_id("new"), - trigger_kind: BackupTriggerKind::Scheduled, - scheduled_at: Some(Utc::now() - Duration::days(1)), - object_key: "codex/2026/07/01/new.dump".to_owned(), - expires_at: None, - }; - repository - .insert_scheduled_record(old_seed.clone()) - .await - .expect("insert old"); - // 旧备份 40 天前完成(释放活跃名额),再插入新备份。 - let now = Utc::now(); - repository.set_completed(&backup_id("old"), now - Duration::days(40)); - repository - .insert_scheduled_record(new_seed.clone()) - .await - .expect("insert new"); - // 新备份 1 天前完成。 - repository.set_completed(&backup_id("new"), now - Duration::days(1)); - // 两个对象都已上传。 - object_store - .objects - .lock() - .expect("objects") - .insert(old_seed.object_key.clone(), DUMP_CONTENT.to_vec()); - object_store - .objects - .lock() - .expect("objects") - .insert(new_seed.object_key.clone(), DUMP_CONTENT.to_vec()); - - // 设置 retention_days = 30。 - repository - .update_schedule_settings( - UpdateBackupScheduleCommand { - schedule_enabled: false, - cron_expression: "0 2 * * *".to_owned(), - schedule_timezone: "Asia/Shanghai".to_owned(), - retention_days: 30, - retention_count: 0, - }, - None, - &system_context(), - ) - .await - .expect("set retention"); - - let task = BackupTask::new(repository.clone(), dump.clone(), object_store.clone()); - task.run_cycle(&CancellationToken::new()) - .await - .expect("run cycle"); - - // 旧备份被硬删除,对象被删除;新备份保留。 - let records = repository.all_records(); - let ids: Vec<_> = records.iter().map(|record| record.id.as_str()).collect(); - assert_eq!(ids, vec![backup_id("new").as_str()]); - assert!(object_store.object(&old_seed.object_key).is_none()); - assert!(object_store.object(&new_seed.object_key).is_some()); -} diff --git a/backend/crates/gateway-admin/tests/backup/task.rs b/backend/crates/gateway-admin/tests/backup/task.rs new file mode 100644 index 000000000..8915d0acb --- /dev/null +++ b/backend/crates/gateway-admin/tests/backup/task.rs @@ -0,0 +1,165 @@ +//! BackupTask 执行链测试:驱动内存 fake 端口跑完整 dump/upload/verify/retention 流程。 + +use std::sync::Arc; + +use chrono::{Duration, Utc}; +use gateway_admin::{ + backup::task::BackupTask, + model::backup::{ + BackupRecordSeed, BackupStatus, BackupTriggerKind, UpdateBackupScheduleCommand, + }, + ports::backup::BackupRepository, +}; +use gateway_core::engine::CancellationToken; + +use super::support::{ + DUMP_CONTENT, FakeBackupRepository, FakeDumpPort, FakeObjectStore, backup_id, + configured_settings, sha256_hex, system_context, +}; + +#[tokio::test] +async fn backup_task_runs_full_dump_upload_verify_pipeline() { + let repository = Arc::new(FakeBackupRepository::new(configured_settings())); + let dump = Arc::new(FakeDumpPort::new()); + let object_store = Arc::new(FakeObjectStore::new()); + + let seed = BackupRecordSeed { + id: backup_id("manual"), + trigger_kind: BackupTriggerKind::Manual, + scheduled_at: None, + object_key: "codex/2026/08/01/manual.dump".to_owned(), + expires_at: None, + }; + repository + .insert_backup_record(seed.clone()) + .await + .expect("insert queued"); + + let task = BackupTask::new(repository.clone(), dump.clone(), object_store.clone()); + task.run_cycle(&CancellationToken::new()) + .await + .expect("run cycle"); + + let records = repository.all_records(); + assert_eq!(records.len(), 1); + let finished = &records[0]; + assert_eq!(finished.status, BackupStatus::Completed); + assert_eq!(finished.size_bytes, Some(DUMP_CONTENT.len() as u64)); + assert_eq!( + finished.sha256.as_deref(), + Some(sha256_hex(DUMP_CONTENT).as_str()) + ); + + // 对象已上传且内容与本地归档一致。 + let stored = object_store + .object(&seed.object_key) + .expect("uploaded object exists"); + assert_eq!(stored.as_slice(), DUMP_CONTENT); +} + +#[tokio::test] +async fn backup_task_marks_failed_when_dump_port_fails() { + let repository = Arc::new(FakeBackupRepository::new(configured_settings())); + let dump = Arc::new(FakeDumpPort::new().fail_next_dump()); + let object_store = Arc::new(FakeObjectStore::new()); + + let seed = BackupRecordSeed { + id: backup_id("fail"), + trigger_kind: BackupTriggerKind::Manual, + scheduled_at: None, + object_key: "codex/2026/08/01/fail.dump".to_owned(), + expires_at: None, + }; + repository + .insert_backup_record(seed.clone()) + .await + .expect("insert queued"); + + let task = BackupTask::new(repository.clone(), dump.clone(), object_store.clone()); + task.run_cycle(&CancellationToken::new()) + .await + .expect("run cycle"); + + let records = repository.all_records(); + let failed = &records[0]; + assert_eq!(failed.status, BackupStatus::Failed); + assert_eq!(failed.error_code.as_deref(), Some("backup.pg_dump_failed")); + assert!(failed.completed_at.is_some()); + // 归档未上传,对象不存在。 + assert!(object_store.object(&seed.object_key).is_none()); +} + +#[tokio::test] +async fn retention_cleans_expired_scheduled_backups() { + let repository = Arc::new(FakeBackupRepository::new(configured_settings())); + let dump = Arc::new(FakeDumpPort::new()); + let object_store = Arc::new(FakeObjectStore::new()); + + // 造一条旧 completed 计划备份(超过 retention_days)与一条新备份。 + let old_seed = BackupRecordSeed { + id: backup_id("old"), + trigger_kind: BackupTriggerKind::Scheduled, + scheduled_at: Some(Utc::now() - Duration::days(40)), + object_key: "codex/2026/06/01/old.dump".to_owned(), + expires_at: None, + }; + let new_seed = BackupRecordSeed { + id: backup_id("new"), + trigger_kind: BackupTriggerKind::Scheduled, + scheduled_at: Some(Utc::now() - Duration::days(1)), + object_key: "codex/2026/07/01/new.dump".to_owned(), + expires_at: None, + }; + repository + .insert_scheduled_record(old_seed.clone()) + .await + .expect("insert old"); + // 旧备份 40 天前完成(释放活跃名额),再插入新备份。 + let now = Utc::now(); + repository.set_completed(&backup_id("old"), now - Duration::days(40)); + repository + .insert_scheduled_record(new_seed.clone()) + .await + .expect("insert new"); + // 新备份 1 天前完成。 + repository.set_completed(&backup_id("new"), now - Duration::days(1)); + // 两个对象都已上传。 + object_store + .objects + .lock() + .expect("objects") + .insert(old_seed.object_key.clone(), DUMP_CONTENT.to_vec()); + object_store + .objects + .lock() + .expect("objects") + .insert(new_seed.object_key.clone(), DUMP_CONTENT.to_vec()); + + // 设置 retention_days = 30。 + repository + .update_schedule_settings( + UpdateBackupScheduleCommand { + schedule_enabled: false, + cron_expression: "0 2 * * *".to_owned(), + schedule_timezone: "Asia/Shanghai".to_owned(), + retention_days: 30, + retention_count: 0, + }, + None, + &system_context(), + ) + .await + .expect("set retention"); + + let task = BackupTask::new(repository.clone(), dump.clone(), object_store.clone()); + task.run_cycle(&CancellationToken::new()) + .await + .expect("run cycle"); + + // 旧备份被硬删除,对象被删除;新备份保留。 + let records = repository.all_records(); + let ids: Vec<_> = records.iter().map(|record| record.id.as_str()).collect(); + assert_eq!(ids, vec![backup_id("new").as_str()]); + assert!(object_store.object(&old_seed.object_key).is_none()); + assert!(object_store.object(&new_seed.object_key).is_some()); +} diff --git a/backend/crates/gateway-admin/tests/main.rs b/backend/crates/gateway-admin/tests/main.rs index e3565c53d..67b2a01f0 100644 --- a/backend/crates/gateway-admin/tests/main.rs +++ b/backend/crates/gateway-admin/tests/main.rs @@ -18,7 +18,7 @@ use gateway_admin::{ }; mod backup; -mod backup_runtime; +mod model; mod use_case; #[test] diff --git a/backend/crates/gateway-admin/tests/model/backup.rs b/backend/crates/gateway-admin/tests/model/backup.rs new file mode 100644 index 000000000..23ed38c54 --- /dev/null +++ b/backend/crates/gateway-admin/tests/model/backup.rs @@ -0,0 +1,104 @@ +//! 备份领域模型(model/backup.rs)的纯逻辑测试。 + +use chrono::{TimeZone as _, Utc}; +use secrecy::SecretString; + +use gateway_admin::model::backup::{ + BackupSettings, BackupStatus, BackupStorageConfig, BackupTriggerKind, build_object_key, +}; + +#[test] +fn status_machine_rejects_invalid_transitions() { + assert!(BackupStatus::Queued.allows_transition_to(BackupStatus::Dumping)); + assert!(BackupStatus::Dumping.allows_transition_to(BackupStatus::Uploading)); + assert!(BackupStatus::Uploading.allows_transition_to(BackupStatus::Completed)); + assert!(BackupStatus::Completed.allows_transition_to(BackupStatus::Deleting)); + assert!(!BackupStatus::Queued.allows_transition_to(BackupStatus::Completed)); + assert!(!BackupStatus::Completed.allows_transition_to(BackupStatus::Dumping)); + assert!(!BackupStatus::Deleting.allows_transition_to(BackupStatus::Failed)); + assert!(!BackupStatus::Deleting.allows_transition_to(BackupStatus::Completed)); +} + +#[test] +fn status_classification() { + assert!(BackupStatus::Queued.is_active()); + assert!(BackupStatus::Uploading.is_active()); + assert!(!BackupStatus::Completed.is_active()); + assert!(BackupStatus::Completed.can_be_deleted()); + assert!(BackupStatus::Failed.can_be_deleted()); + assert!(!BackupStatus::Queued.can_be_deleted()); +} + +#[test] +fn object_key_shape() { + let at = Utc.with_ymd_and_hms(2026, 8, 1, 12, 30, 0).unwrap(); + let key = build_object_key("production/backups", "backup_abc", at).unwrap(); + assert_eq!( + key, + "production/backups/2026/08/01/codex-proxy-rs_20260801_123000_abc.dump" + ); +} + +#[test] +fn object_key_rejects_dangerous_prefixes() { + let at = Utc::now(); + assert!(build_object_key("/leading", "backup_x", at).is_err()); + assert!(build_object_key("a/../b", "backup_x", at).is_err()); + assert!(build_object_key("a\\b", "backup_x", at).is_err()); + assert!(build_object_key("a\u{0001}b", "backup_x", at).is_err()); + assert!(build_object_key("", "backup_x", at).is_err()); + assert!(build_object_key("trailing//", "backup_x", at).is_ok()); +} + +#[test] +fn trigger_and_status_roundtrip() { + for status in [ + BackupStatus::Queued, + BackupStatus::Dumping, + BackupStatus::Uploading, + BackupStatus::Completed, + BackupStatus::Failed, + BackupStatus::Deleting, + ] { + assert_eq!(BackupStatus::parse(status.as_str()), Some(status)); + } + assert_eq!( + BackupTriggerKind::parse("manual"), + Some(BackupTriggerKind::Manual) + ); + assert_eq!( + BackupTriggerKind::parse("scheduled"), + Some(BackupTriggerKind::Scheduled) + ); + assert_eq!(BackupTriggerKind::parse("unknown"), None); + assert_eq!(BackupStatus::parse("skipped"), None); + assert_eq!(BackupStatus::parse("deleted"), None); + assert_eq!(BackupStatus::parse("unknown"), None); +} + +#[test] +fn config_from_settings_redacts_secret_in_debug() { + let mut settings = BackupSettings { + storage_revision: 3, + endpoint: Some("https://example.com".to_owned()), + region: Some("auto".to_owned()), + bucket: Some("b".to_owned()), + access_key_id: Some("ak".to_owned()), + secret_access_key: Some(SecretString::from("sk-secret")), + prefix: Some("p".to_owned()), + force_path_style: false, + schedule_enabled: false, + cron_expression: None, + schedule_timezone: None, + retention_days: 0, + retention_count: 0, + next_run_at: None, + last_verified_at: None, + updated_at: Utc::now(), + }; + assert!(!format!("{:?}", settings).contains("sk-secret")); + let config = BackupStorageConfig::from_settings(&settings).unwrap(); + assert!(!format!("{config:?}").contains("sk-secret")); + settings.endpoint = None; + assert!(BackupStorageConfig::from_settings(&settings).is_none()); +} diff --git a/backend/crates/gateway-admin/tests/model/mod.rs b/backend/crates/gateway-admin/tests/model/mod.rs new file mode 100644 index 000000000..25d24e700 --- /dev/null +++ b/backend/crates/gateway-admin/tests/model/mod.rs @@ -0,0 +1,3 @@ +//! 模型模块测试:镜像 src/model/ 中带逻辑的模块。 + +mod backup; diff --git a/backend/crates/gateway-admin/tests/use_case/backup.rs b/backend/crates/gateway-admin/tests/use_case/backup.rs index c4faa0d9f..638d762ae 100644 --- a/backend/crates/gateway-admin/tests/use_case/backup.rs +++ b/backend/crates/gateway-admin/tests/use_case/backup.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use gateway_admin::model::backup::BackupStatus; use gateway_admin::ports::backup::BackupStorePorts; -use crate::backup_runtime::{ +use crate::backup::support::{ FakeAuthStore, FakeBackupRepository, FakeDumpPort, FakeObjectStore, configured_settings, system_context, }; diff --git a/backend/crates/gateway-store/tests/main.rs b/backend/crates/gateway-store/tests/main.rs index f6799a403..3ec752bac 100644 --- a/backend/crates/gateway-store/tests/main.rs +++ b/backend/crates/gateway-store/tests/main.rs @@ -1,5 +1,3 @@ -mod coordination_buffer; -mod execution_buffer; mod postgres; mod redis; mod support; diff --git a/backend/crates/gateway-store/tests/execution_buffer.rs b/backend/crates/gateway-store/tests/postgres/execution_buffer.rs similarity index 100% rename from backend/crates/gateway-store/tests/execution_buffer.rs rename to backend/crates/gateway-store/tests/postgres/execution_buffer.rs diff --git a/backend/crates/gateway-store/tests/postgres/mod.rs b/backend/crates/gateway-store/tests/postgres/mod.rs index 68e45a490..6a343574d 100644 --- a/backend/crates/gateway-store/tests/postgres/mod.rs +++ b/backend/crates/gateway-store/tests/postgres/mod.rs @@ -12,6 +12,7 @@ mod admission_recovery; mod backup; mod client_keys; mod execution; +mod execution_buffer; mod observability; mod ops_events; mod provider_accounts; diff --git a/backend/crates/gateway-store/tests/coordination_buffer.rs b/backend/crates/gateway-store/tests/redis/coordination_buffer.rs similarity index 100% rename from backend/crates/gateway-store/tests/coordination_buffer.rs rename to backend/crates/gateway-store/tests/redis/coordination_buffer.rs diff --git a/backend/crates/gateway-store/tests/redis/mod.rs b/backend/crates/gateway-store/tests/redis/mod.rs index b140fcc62..fdff711cf 100644 --- a/backend/crates/gateway-store/tests/redis/mod.rs +++ b/backend/crates/gateway-store/tests/redis/mod.rs @@ -1,4 +1,5 @@ mod client_admission; +mod coordination_buffer; mod credential_cooldown; mod credential_leases; mod credential_state;