mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 01:25:33 +08:00
Webdav (#923)
* feat: WebDAV backup/restore - Add WebDAV test/backup/restore commands and settings\n- Fix ja i18n missing keys; decode PROPFIND href as UTF-8\n- Stabilize Windows prompt auto-import tests via CC_SWITCH_TEST_HOME * chore: format and minor cleanups * fix: update build config * feat(webdav): unify sync UX and hardening fixes * fix(webdav): harden sync flow and stabilize sync UX/tests * fix(webdav): add resource limits to skills.zip extraction Prevent zip bomb / resource exhaustion by enforcing: - MAX_EXTRACT_ENTRIES (10,000 files) - MAX_EXTRACT_BYTES (512 MB cumulative) * refactor(webdav): drop deviceId and display deviceName only --------- Co-authored-by: small-lovely-cat <77799160+small-lovely-cat@users.noreply.github.com> Co-authored-by: saladday <1203511142@qq.com>
This commit is contained in:
@@ -5,10 +5,15 @@ use std::path::PathBuf;
|
||||
use tauri::State;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
use crate::commands::sync_support::{
|
||||
post_sync_warning_from_result, run_post_import_sync, success_payload_with_warning,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
use crate::services::provider::ProviderService;
|
||||
use crate::store::AppState;
|
||||
|
||||
// ─── File import/export ──────────────────────────────────────
|
||||
|
||||
/// 导出数据库为 SQL 备份
|
||||
#[tauri::command]
|
||||
pub async fn export_config_to_file(
|
||||
@@ -37,27 +42,15 @@ pub async fn import_config_from_file(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Value, String> {
|
||||
let db = state.db.clone();
|
||||
let db_for_state = db.clone();
|
||||
let db_for_sync = db.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let path_buf = PathBuf::from(&filePath);
|
||||
let backup_id = db.import_sql(&path_buf)?;
|
||||
|
||||
// 导入后同步当前供应商到各自的 live 配置
|
||||
let app_state = AppState::new(db_for_state);
|
||||
if let Err(err) = ProviderService::sync_current_to_live(&app_state) {
|
||||
log::warn!("导入后同步 live 配置失败: {err}");
|
||||
let warning = post_sync_warning_from_result(Ok(run_post_import_sync(db_for_sync)));
|
||||
if let Some(msg) = warning.as_ref() {
|
||||
log::warn!("[Import] post-import sync warning: {msg}");
|
||||
}
|
||||
|
||||
// 重新加载设置到内存缓存,确保导入的设置生效
|
||||
if let Err(err) = crate::settings::reload_settings() {
|
||||
log::warn!("导入后重载设置失败: {err}");
|
||||
}
|
||||
|
||||
Ok::<_, AppError>(json!({
|
||||
"success": true,
|
||||
"message": "SQL imported successfully",
|
||||
"backupId": backup_id
|
||||
}))
|
||||
Ok::<_, AppError>(success_payload_with_warning(backup_id, warning))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("导入配置失败: {e}"))?
|
||||
@@ -80,6 +73,8 @@ pub async fn sync_current_providers_live(state: State<'_, AppState>) -> Result<V
|
||||
.map_err(|e: AppError| e.to_string())
|
||||
}
|
||||
|
||||
// ─── File dialogs ────────────────────────────────────────────
|
||||
|
||||
/// 保存文件对话框
|
||||
#[tauri::command]
|
||||
pub async fn save_file_dialog<R: tauri::Runtime>(
|
||||
|
||||
@@ -17,7 +17,9 @@ mod session_manager;
|
||||
mod settings;
|
||||
pub mod skill;
|
||||
mod stream_check;
|
||||
mod sync_support;
|
||||
mod usage;
|
||||
mod webdav_sync;
|
||||
|
||||
pub use config::*;
|
||||
pub use deeplink::*;
|
||||
@@ -37,3 +39,4 @@ pub use settings::*;
|
||||
pub use skill::*;
|
||||
pub use stream_check::*;
|
||||
pub use usage::*;
|
||||
pub use webdav_sync::*;
|
||||
|
||||
@@ -2,16 +2,28 @@
|
||||
|
||||
use tauri::AppHandle;
|
||||
|
||||
fn merge_settings_for_save(
|
||||
mut incoming: crate::settings::AppSettings,
|
||||
existing: &crate::settings::AppSettings,
|
||||
) -> crate::settings::AppSettings {
|
||||
if incoming.webdav_sync.is_none() {
|
||||
incoming.webdav_sync = existing.webdav_sync.clone();
|
||||
}
|
||||
incoming
|
||||
}
|
||||
|
||||
/// 获取设置
|
||||
#[tauri::command]
|
||||
pub async fn get_settings() -> Result<crate::settings::AppSettings, String> {
|
||||
Ok(crate::settings::get_settings())
|
||||
Ok(crate::settings::get_settings_for_frontend())
|
||||
}
|
||||
|
||||
/// 保存设置
|
||||
#[tauri::command]
|
||||
pub async fn save_settings(settings: crate::settings::AppSettings) -> Result<bool, String> {
|
||||
crate::settings::update_settings(settings).map_err(|e| e.to_string())?;
|
||||
let existing = crate::settings::get_settings();
|
||||
let merged = merge_settings_for_save(settings, &existing);
|
||||
crate::settings::update_settings(merged).map_err(|e| e.to_string())?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -54,6 +66,58 @@ pub async fn set_auto_launch(enabled: bool) -> Result<bool, String> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::merge_settings_for_save;
|
||||
use crate::settings::{AppSettings, WebDavSyncSettings};
|
||||
|
||||
#[test]
|
||||
fn save_settings_should_preserve_existing_webdav_when_payload_omits_it() {
|
||||
let mut existing = AppSettings::default();
|
||||
existing.webdav_sync = Some(WebDavSyncSettings {
|
||||
base_url: "https://dav.example.com".to_string(),
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
});
|
||||
|
||||
let incoming = AppSettings::default();
|
||||
let merged = merge_settings_for_save(incoming, &existing);
|
||||
|
||||
assert!(merged.webdav_sync.is_some());
|
||||
assert_eq!(
|
||||
merged.webdav_sync.as_ref().map(|v| v.base_url.as_str()),
|
||||
Some("https://dav.example.com")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_settings_should_keep_incoming_webdav_when_present() {
|
||||
let mut existing = AppSettings::default();
|
||||
existing.webdav_sync = Some(WebDavSyncSettings {
|
||||
base_url: "https://dav.old.example.com".to_string(),
|
||||
username: "old".to_string(),
|
||||
password: "old-pass".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
});
|
||||
|
||||
let mut incoming = AppSettings::default();
|
||||
incoming.webdav_sync = Some(WebDavSyncSettings {
|
||||
base_url: "https://dav.new.example.com".to_string(),
|
||||
username: "new".to_string(),
|
||||
password: "new-pass".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
});
|
||||
|
||||
let merged = merge_settings_for_save(incoming, &existing);
|
||||
|
||||
assert_eq!(
|
||||
merged.webdav_sync.as_ref().map(|v| v.base_url.as_str()),
|
||||
Some("https://dav.new.example.com")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取开机自启状态
|
||||
#[tauri::command]
|
||||
pub async fn get_auto_launch_status() -> Result<bool, String> {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::error::AppError;
|
||||
use crate::services::provider::ProviderService;
|
||||
use crate::settings;
|
||||
use crate::store::AppState;
|
||||
|
||||
pub(crate) fn run_post_import_sync(db: Arc<Database>) -> Result<(), AppError> {
|
||||
let app_state = AppState::new(db);
|
||||
ProviderService::sync_current_to_live(&app_state)?;
|
||||
settings::reload_settings()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_sync_warning<E: std::fmt::Display>(err: E) -> String {
|
||||
AppError::localized(
|
||||
"sync.post_operation_sync_failed",
|
||||
format!("后置同步状态失败: {err}"),
|
||||
format!("Post-operation synchronization failed: {err}"),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn post_sync_warning_from_result(
|
||||
result: Result<Result<(), AppError>, String>,
|
||||
) -> Option<String> {
|
||||
match result {
|
||||
Ok(Ok(())) => None,
|
||||
Ok(Err(err)) => Some(post_sync_warning(err)),
|
||||
Err(err) => Some(post_sync_warning(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attach_warning(mut value: Value, warning: Option<String>) -> Value {
|
||||
if let Some(message) = warning {
|
||||
if let Some(obj) = value.as_object_mut() {
|
||||
obj.insert("warning".to_string(), Value::String(message));
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
pub(crate) fn success_payload_with_warning(backup_id: String, warning: Option<String>) -> Value {
|
||||
attach_warning(
|
||||
json!({
|
||||
"success": true,
|
||||
"message": "SQL imported successfully",
|
||||
"backupId": backup_id
|
||||
}),
|
||||
warning,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{attach_warning, post_sync_warning_from_result};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn post_sync_warning_from_result_returns_none_on_success() {
|
||||
let warning = post_sync_warning_from_result(Ok(Ok(())));
|
||||
assert!(warning.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_sync_warning_from_result_returns_some_on_sync_error() {
|
||||
let warning =
|
||||
post_sync_warning_from_result(Ok(Err(crate::error::AppError::Config("boom".into()))));
|
||||
assert!(warning.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn post_sync_warning_from_result_returns_some_on_join_error() {
|
||||
let handle = tokio::spawn(async move {
|
||||
panic!("forced join error");
|
||||
});
|
||||
let join_err = handle.await.expect_err("task should panic");
|
||||
let warning = post_sync_warning_from_result(Err(join_err.to_string()));
|
||||
assert!(warning.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_warning_adds_warning_without_dropping_existing_fields() {
|
||||
let payload = json!({ "status": "downloaded" });
|
||||
let updated = attach_warning(payload, Some("post sync warning".to_string()));
|
||||
assert_eq!(
|
||||
updated.get("status").and_then(|v| v.as_str()),
|
||||
Some("downloaded")
|
||||
);
|
||||
assert_eq!(
|
||||
updated.get("warning").and_then(|v| v.as_str()),
|
||||
Some("post sync warning")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::future::Future;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::State;
|
||||
|
||||
use crate::commands::sync_support::{
|
||||
attach_warning, post_sync_warning_from_result, run_post_import_sync,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
use crate::services::webdav_sync as webdav_sync_service;
|
||||
use crate::settings::{self, WebDavSyncSettings};
|
||||
use crate::store::AppState;
|
||||
|
||||
fn persist_sync_error(settings: &mut WebDavSyncSettings, error: &AppError) {
|
||||
settings.status.last_error = Some(error.to_string());
|
||||
let _ = settings::update_webdav_sync_status(settings.status.clone());
|
||||
}
|
||||
|
||||
fn webdav_not_configured_error() -> String {
|
||||
AppError::localized(
|
||||
"webdav.sync.not_configured",
|
||||
"未配置 WebDAV 同步",
|
||||
"WebDAV sync is not configured.",
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn webdav_sync_disabled_error() -> String {
|
||||
AppError::localized(
|
||||
"webdav.sync.disabled",
|
||||
"WebDAV 同步未启用",
|
||||
"WebDAV sync is disabled.",
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn require_enabled_webdav_settings() -> Result<WebDavSyncSettings, String> {
|
||||
let settings = settings::get_webdav_sync_settings().ok_or_else(webdav_not_configured_error)?;
|
||||
if !settings.enabled {
|
||||
return Err(webdav_sync_disabled_error());
|
||||
}
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn resolve_password_for_request(
|
||||
mut incoming: WebDavSyncSettings,
|
||||
existing: Option<WebDavSyncSettings>,
|
||||
preserve_empty_password: bool,
|
||||
) -> WebDavSyncSettings {
|
||||
if let Some(existing_settings) = existing {
|
||||
if preserve_empty_password && incoming.password.is_empty() {
|
||||
incoming.password = existing_settings.password;
|
||||
}
|
||||
}
|
||||
incoming
|
||||
}
|
||||
|
||||
fn webdav_sync_mutex() -> &'static tokio::sync::Mutex<()> {
|
||||
static LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
|
||||
}
|
||||
|
||||
async fn run_with_webdav_lock<T, Fut>(operation: Fut) -> Result<T, AppError>
|
||||
where
|
||||
Fut: Future<Output = Result<T, AppError>>,
|
||||
{
|
||||
let result = {
|
||||
let _guard = webdav_sync_mutex().lock().await;
|
||||
operation.await
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
fn map_sync_result<T, F>(result: Result<T, AppError>, on_error: F) -> Result<T, String>
|
||||
where
|
||||
F: FnOnce(&AppError),
|
||||
{
|
||||
match result {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => {
|
||||
on_error(&err);
|
||||
Err(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_test_connection(
|
||||
settings: WebDavSyncSettings,
|
||||
#[allow(non_snake_case)] preserveEmptyPassword: Option<bool>,
|
||||
) -> Result<Value, String> {
|
||||
let preserve_empty = preserveEmptyPassword.unwrap_or(true);
|
||||
let resolved = resolve_password_for_request(
|
||||
settings,
|
||||
settings::get_webdav_sync_settings(),
|
||||
preserve_empty,
|
||||
);
|
||||
webdav_sync_service::check_connection(&resolved)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"message": "WebDAV connection ok"
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_sync_upload(state: State<'_, AppState>) -> Result<Value, String> {
|
||||
let db = state.db.clone();
|
||||
let mut settings = require_enabled_webdav_settings()?;
|
||||
|
||||
let result = run_with_webdav_lock(webdav_sync_service::upload(&db, &mut settings)).await;
|
||||
map_sync_result(result, |error| persist_sync_error(&mut settings, error))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_sync_download(state: State<'_, AppState>) -> Result<Value, String> {
|
||||
let db = state.db.clone();
|
||||
let db_for_sync = db.clone();
|
||||
let mut settings = require_enabled_webdav_settings()?;
|
||||
|
||||
let sync_result = run_with_webdav_lock(webdav_sync_service::download(&db, &mut settings)).await;
|
||||
let mut result = map_sync_result(sync_result, |error| {
|
||||
persist_sync_error(&mut settings, error)
|
||||
})?;
|
||||
|
||||
// Post-download sync is best-effort: snapshot restore has already succeeded.
|
||||
let warning = post_sync_warning_from_result(
|
||||
tauri::async_runtime::spawn_blocking(move || run_post_import_sync(db_for_sync))
|
||||
.await
|
||||
.map_err(|e| e.to_string()),
|
||||
);
|
||||
if let Some(msg) = warning.as_ref() {
|
||||
log::warn!("[WebDAV] post-download sync warning: {msg}");
|
||||
}
|
||||
result = attach_warning(result, warning);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_sync_save_settings(
|
||||
settings: WebDavSyncSettings,
|
||||
#[allow(non_snake_case)] passwordTouched: Option<bool>,
|
||||
) -> Result<Value, String> {
|
||||
let password_touched = passwordTouched.unwrap_or(false);
|
||||
let existing = settings::get_webdav_sync_settings();
|
||||
let mut sync_settings =
|
||||
resolve_password_for_request(settings, existing.clone(), !password_touched);
|
||||
|
||||
// Preserve server-owned fields that the frontend does not manage
|
||||
if let Some(existing_settings) = existing {
|
||||
sync_settings.status = existing_settings.status;
|
||||
}
|
||||
|
||||
sync_settings.normalize();
|
||||
sync_settings.validate().map_err(|e| e.to_string())?;
|
||||
settings::set_webdav_sync_settings(Some(sync_settings)).map_err(|e| e.to_string())?;
|
||||
Ok(json!({ "success": true }))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn webdav_sync_fetch_remote_info() -> Result<Value, String> {
|
||||
let settings = require_enabled_webdav_settings()?;
|
||||
let info = webdav_sync_service::fetch_remote_info(&settings)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(info.unwrap_or(json!({ "empty": true })))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
map_sync_result, persist_sync_error, require_enabled_webdav_settings,
|
||||
resolve_password_for_request, run_with_webdav_lock, webdav_sync_mutex,
|
||||
};
|
||||
use crate::error::AppError;
|
||||
use crate::settings::{AppSettings, WebDavSyncSettings};
|
||||
use serial_test::serial;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn webdav_sync_mutex_is_singleton() {
|
||||
let a = webdav_sync_mutex() as *const _;
|
||||
let b = webdav_sync_mutex() as *const _;
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn webdav_sync_mutex_serializes_concurrent_access() {
|
||||
let guard = webdav_sync_mutex().lock().await;
|
||||
let acquired = Arc::new(AtomicBool::new(false));
|
||||
let acquired_bg = Arc::clone(&acquired);
|
||||
|
||||
let waiter = tokio::spawn(async move {
|
||||
let _inner_guard = webdav_sync_mutex().lock().await;
|
||||
acquired_bg.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(40)).await;
|
||||
assert!(!acquired.load(Ordering::SeqCst));
|
||||
|
||||
drop(guard);
|
||||
tokio::time::timeout(Duration::from_secs(1), waiter)
|
||||
.await
|
||||
.expect("background task should complete after lock release")
|
||||
.expect("background task should not panic");
|
||||
|
||||
assert!(acquired.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn map_sync_result_runs_error_handler_after_lock_release() {
|
||||
let result = run_with_webdav_lock(async {
|
||||
Err::<(), AppError>(AppError::Config("boom".to_string()))
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut lock_released = false;
|
||||
let mapped = map_sync_result(result, |_| {
|
||||
lock_released = webdav_sync_mutex().try_lock().is_ok();
|
||||
});
|
||||
|
||||
assert!(mapped.is_err());
|
||||
assert!(lock_released);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_password_for_request_preserves_existing_when_requested() {
|
||||
let incoming = WebDavSyncSettings {
|
||||
base_url: "https://dav.example.com".to_string(),
|
||||
username: "alice".to_string(),
|
||||
password: String::new(),
|
||||
..WebDavSyncSettings::default()
|
||||
};
|
||||
let existing = Some(WebDavSyncSettings {
|
||||
password: "secret".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
});
|
||||
let resolved = resolve_password_for_request(incoming, existing, true);
|
||||
assert_eq!(resolved.password, "secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_password_for_request_allows_explicit_empty_password() {
|
||||
let incoming = WebDavSyncSettings {
|
||||
base_url: "https://dav.example.com".to_string(),
|
||||
username: "alice".to_string(),
|
||||
password: String::new(),
|
||||
..WebDavSyncSettings::default()
|
||||
};
|
||||
let existing = Some(WebDavSyncSettings {
|
||||
password: "secret".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
});
|
||||
let resolved = resolve_password_for_request(incoming, existing, false);
|
||||
assert!(resolved.password.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn persist_sync_error_updates_status_without_overwriting_credentials() {
|
||||
let test_home = std::env::temp_dir().join("cc-switch-sync-error-status-test");
|
||||
let _ = std::fs::remove_dir_all(&test_home);
|
||||
std::fs::create_dir_all(&test_home).expect("create test home");
|
||||
std::env::set_var("CC_SWITCH_TEST_HOME", &test_home);
|
||||
|
||||
crate::settings::update_settings(AppSettings::default()).expect("reset settings");
|
||||
let mut current = WebDavSyncSettings {
|
||||
enabled: true,
|
||||
base_url: "https://dav.example.com/dav/".to_string(),
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
remote_root: "cc-switch-sync".to_string(),
|
||||
profile: "default".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
};
|
||||
crate::settings::set_webdav_sync_settings(Some(current.clone()))
|
||||
.expect("seed webdav settings");
|
||||
|
||||
persist_sync_error(
|
||||
&mut current,
|
||||
&crate::error::AppError::Config("boom".to_string()),
|
||||
);
|
||||
|
||||
let after = crate::settings::get_webdav_sync_settings().expect("read webdav settings");
|
||||
assert_eq!(after.base_url, "https://dav.example.com/dav/");
|
||||
assert_eq!(after.username, "alice");
|
||||
assert_eq!(after.password, "secret");
|
||||
assert_eq!(after.remote_root, "cc-switch-sync");
|
||||
assert_eq!(after.profile, "default");
|
||||
assert!(
|
||||
after
|
||||
.status
|
||||
.last_error
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("boom"),
|
||||
"status error should be updated"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn require_enabled_webdav_settings_rejects_disabled_config() {
|
||||
let test_home = std::env::temp_dir().join("cc-switch-sync-enabled-disabled-test");
|
||||
let _ = std::fs::remove_dir_all(&test_home);
|
||||
std::fs::create_dir_all(&test_home).expect("create test home");
|
||||
std::env::set_var("CC_SWITCH_TEST_HOME", &test_home);
|
||||
|
||||
crate::settings::update_settings(AppSettings::default()).expect("reset settings");
|
||||
crate::settings::set_webdav_sync_settings(Some(WebDavSyncSettings {
|
||||
enabled: false,
|
||||
base_url: "https://dav.example.com/dav/".to_string(),
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
}))
|
||||
.expect("seed disabled webdav settings");
|
||||
|
||||
let err = require_enabled_webdav_settings().expect_err("disabled settings should fail");
|
||||
assert!(
|
||||
err.contains("disabled") || err.contains("未启用"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn require_enabled_webdav_settings_returns_settings_when_enabled() {
|
||||
let test_home = std::env::temp_dir().join("cc-switch-sync-enabled-ok-test");
|
||||
let _ = std::fs::remove_dir_all(&test_home);
|
||||
std::fs::create_dir_all(&test_home).expect("create test home");
|
||||
std::env::set_var("CC_SWITCH_TEST_HOME", &test_home);
|
||||
|
||||
crate::settings::update_settings(AppSettings::default()).expect("reset settings");
|
||||
crate::settings::set_webdav_sync_settings(Some(WebDavSyncSettings {
|
||||
enabled: true,
|
||||
base_url: "https://dav.example.com/dav/".to_string(),
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
..WebDavSyncSettings::default()
|
||||
}))
|
||||
.expect("seed enabled webdav settings");
|
||||
|
||||
let settings =
|
||||
require_enabled_webdav_settings().expect("enabled settings should be accepted");
|
||||
assert!(settings.enabled);
|
||||
assert_eq!(settings.base_url, "https://dav.example.com/dav/");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user