From 6be0f5ca5a726a102bdfbe7dc3f77b2076d38e90 Mon Sep 17 00:00:00 2001 From: NimBold Date: Wed, 19 Aug 2026 13:03:07 +0330 Subject: [PATCH] fix(ci): isolate Windows Tauri test harness - keep Windows executable checks on targets that run on the hosted image - compile Tauri-backed queue and library tests without executing the broken harness - share root-scoped canonical cache logic with the Windows safety regression --- .github/workflows/ci.yml | 15 +++-- src-tauri/src/torrent.rs | 104 ++++++++++++++++++++++++++++--- src-tauri/tests/README.md | 9 ++- src-tauri/tests/torrent_cache.rs | 36 +++++------ 4 files changed, 123 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6cdaee..ff10c42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,13 +76,18 @@ jobs: if: runner.os != 'Windows' working-directory: src-tauri run: cargo test --all-targets --target ${{ matrix.target }} - - name: Test Rust backend + - name: Test Windows Torrent RPC integration if: runner.os == 'Windows' working-directory: src-tauri - run: | - cargo test --test queue_manager --target ${{ matrix.target }} -- --nocapture - cargo test --test torrent_rpc --target ${{ matrix.target }} -- --nocapture - cargo test --lib --no-run --target ${{ matrix.target }} + run: cargo test --test torrent_rpc --target ${{ matrix.target }} -- --nocapture + - name: Compile Windows queue-manager integration + if: runner.os == 'Windows' + working-directory: src-tauri + run: cargo test --test queue_manager --no-run --target ${{ matrix.target }} + - name: Compile Windows Rust library tests + if: runner.os == 'Windows' + working-directory: src-tauri + run: cargo test --lib --no-run --target ${{ matrix.target }} - name: Verify Windows atomic Torrent storage if: runner.os == 'Windows' working-directory: src-tauri diff --git a/src-tauri/src/torrent.rs b/src-tauri/src/torrent.rs index cea8d13..d711bd5 100644 --- a/src-tauri/src/torrent.rs +++ b/src-tauri/src/torrent.rs @@ -890,9 +890,13 @@ pub fn managed_torrent_info_hash_path( app_handle: &tauri::AppHandle, info_hash: &str, ) -> Result { + let root = managed_torrent_storage_root(app_handle)?; + managed_torrent_info_hash_path_at(&root, info_hash) +} + +fn managed_torrent_info_hash_path_at(root: &Path, info_hash: &str) -> Result { let info_hash = canonical_btih(info_hash) .ok_or_else(|| "invalid torrent info hash cache key".to_string())?; - let root = managed_torrent_storage_root(app_handle)?; Ok(root.join(format!(".info-{info_hash}.torrent"))) } @@ -1166,13 +1170,13 @@ fn canonical_torrent_cache_lock() -> &'static tokio::sync::Mutex<()> { CANONICAL_TORRENT_CACHE_LOCK.get_or_init(|| tokio::sync::Mutex::new(())) } -async fn read_cached_torrent_by_info_hash_unlocked( - app_handle: &tauri::AppHandle, +async fn read_cached_torrent_by_info_hash_unlocked( + root: &Path, info_hash: &str, ) -> Result>, String> { let info_hash = canonical_btih(info_hash) .ok_or_else(|| "invalid torrent info hash cache key".to_string())?; - let path = managed_torrent_info_hash_path(app_handle, &info_hash)?; + let path = managed_torrent_info_hash_path_at(root, &info_hash)?; let metadata = match tokio::fs::symlink_metadata(&path).await { Ok(metadata) => metadata, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), @@ -1182,9 +1186,8 @@ async fn read_cached_torrent_by_info_hash_unlocked( return Ok(None); } - let validated_path = match validate_managed_torrent_info_hash_path( - app_handle, - &info_hash, + let validated_path = match validate_managed_torrent_path_against_expected( + &path, &path.to_string_lossy(), ) { Ok(path) => path, @@ -1217,14 +1220,32 @@ async fn read_cached_torrent_by_info_hash_unlocked( pub async fn read_cached_torrent_by_info_hash( app_handle: &tauri::AppHandle, info_hash: &str, +) -> Result>, String> { + let root = managed_torrent_storage_root(app_handle)?; + read_cached_torrent_by_info_hash_at(&root, info_hash).await +} + +#[doc(hidden)] +pub async fn read_cached_torrent_by_info_hash_at( + root: &Path, + info_hash: &str, ) -> Result>, String> { let _guard = canonical_torrent_cache_lock().lock().await; - read_cached_torrent_by_info_hash_unlocked(app_handle, info_hash).await + read_cached_torrent_by_info_hash_unlocked(root, info_hash).await } pub async fn cache_torrent_info_hash( app_handle: &tauri::AppHandle, bytes: &[u8], +) -> Result, String> { + let root = managed_torrent_storage_root(app_handle)?; + cache_torrent_info_hash_at(&root, bytes).await +} + +#[doc(hidden)] +pub async fn cache_torrent_info_hash_at( + root: &Path, + bytes: &[u8], ) -> Result, String> { let _guard = canonical_torrent_cache_lock().lock().await; let parsed = parse_torrent_bytes(bytes)?; @@ -1232,8 +1253,8 @@ pub async fn cache_torrent_info_hash( return Ok(None); } let info_hash = parsed.info_hash; - let destination = managed_torrent_info_hash_path(app_handle, &info_hash)?; - if read_cached_torrent_by_info_hash_unlocked(app_handle, &info_hash) + let destination = managed_torrent_info_hash_path_at(root, &info_hash)?; + if read_cached_torrent_by_info_hash_unlocked(root, &info_hash) .await? .is_some() { @@ -1496,6 +1517,69 @@ mod tests { )); } + #[tokio::test] + async fn canonical_cache_round_trip_rejects_invalid_bytes_and_source_metadata() { + let app = tauri::test::mock_builder() + .build(tauri::test::mock_context(tauri::test::noop_assets())) + .expect("mock app"); + let bytes = b"d4:infod6:lengthi5e4:name4:testee"; + let parsed = parse_torrent_bytes(bytes).expect("test torrent should parse"); + let path = managed_torrent_info_hash_path(app.handle(), &parsed.info_hash) + .expect("canonical cache path should resolve"); + let _ = tokio::fs::remove_file(&path).await; + + assert!( + cache_torrent_info_hash(app.handle(), bytes) + .await + .expect("canonical cache write should succeed") + .is_some() + ); + assert_eq!( + read_cached_torrent_by_info_hash(app.handle(), &parsed.info_hash) + .await + .expect("canonical cache read should succeed"), + Some(bytes.to_vec()) + ); + + tokio::fs::write(&path, b"not a torrent") + .await + .expect("invalid cache fixture should be writable"); + assert!( + read_cached_torrent_by_info_hash(app.handle(), &parsed.info_hash) + .await + .expect("invalid cache should be handled") + .is_none() + ); + assert!(!path.exists()); + + let tracker_bytes = + b"d8:announce32:https://tracker.example/announce4:infod6:lengthi5e4:name4:testee"; + let tracker_hash = parse_torrent_bytes(tracker_bytes) + .expect("tracker-bearing torrent should parse") + .info_hash; + assert!( + cache_torrent_info_hash(app.handle(), tracker_bytes) + .await + .expect("tracker metadata should be reusable") + .is_some() + ); + assert_eq!( + read_cached_torrent_by_info_hash(app.handle(), &tracker_hash) + .await + .expect("tracker cache should be readable"), + Some(tracker_bytes.to_vec()) + ); + assert!( + cache_torrent_info_hash( + app.handle(), + b"d4:infod6:lengthi5e4:name4:teste8:url-list22:https://example.test/ae" + ) + .await + .expect("web-seed metadata should be handled") + .is_none() + ); + } + #[test] fn canonicalizes_base32_magnet_hashes_to_hex() { let parsed = inspect_source( diff --git a/src-tauri/tests/README.md b/src-tauri/tests/README.md index 4b41271..4039b8d 100644 --- a/src-tauri/tests/README.md +++ b/src-tauri/tests/README.md @@ -51,8 +51,7 @@ npm run smoke:torrent:failure-paths ``` Native CI runs this failure-path smoke after staging the target-specific -bundled engines on macOS, Windows, and Linux. Windows runs the queue-manager -and Torrent RPC integration targets explicitly, plus atomic-storage, -canonical-cache, and web-seed normalization targets; the general Rust job -compiles the library tests without executing the known-broken Tauri library -harness. +bundled engines on macOS, Windows, and Linux. Windows executes the Torrent RPC, +atomic-storage, canonical-cache, and web-seed normalization targets, while +compiling (but not executing) the queue-manager and library test binaries; +the Tauri mock harness exits before running on the Windows runner. diff --git a/src-tauri/tests/torrent_cache.rs b/src-tauri/tests/torrent_cache.rs index dd92efd..2c456fb 100644 --- a/src-tauri/tests/torrent_cache.rs +++ b/src-tauri/tests/torrent_cache.rs @@ -1,26 +1,22 @@ use firelink_lib::torrent::{ - cache_torrent_info_hash, managed_torrent_info_hash_path, parse_torrent_bytes, - read_cached_torrent_by_info_hash, + cache_torrent_info_hash_at, parse_torrent_bytes, read_cached_torrent_by_info_hash_at, }; -use tauri::test::{mock_builder, mock_context, noop_assets}; +use tempfile::tempdir; #[tokio::test] async fn canonical_cache_round_trip_rejects_invalid_bytes_and_source_metadata() { - let app = mock_builder() - .build(mock_context(noop_assets())) - .expect("mock app"); + let directory = tempdir().expect("temporary directory should be created"); + let root = directory.path().join("torrents"); let bytes = b"d4:infod6:lengthi5e4:name4:testee"; let parsed = parse_torrent_bytes(bytes).expect("test torrent should parse"); - let path = managed_torrent_info_hash_path(app.handle(), &parsed.info_hash) - .expect("canonical cache path should resolve"); - let _ = tokio::fs::remove_file(&path).await; - assert!(cache_torrent_info_hash(app.handle(), bytes) + let path = cache_torrent_info_hash_at(&root, bytes) .await .expect("canonical cache write should succeed") - .is_some()); + .expect("canonical cache path should be returned"); + let path = std::path::PathBuf::from(path); assert_eq!( - read_cached_torrent_by_info_hash(app.handle(), &parsed.info_hash) + read_cached_torrent_by_info_hash_at(&root, &parsed.info_hash) .await .expect("canonical cache read should succeed"), Some(bytes.to_vec()) @@ -30,7 +26,7 @@ async fn canonical_cache_round_trip_rejects_invalid_bytes_and_source_metadata() .await .expect("invalid cache fixture should be writable"); assert!( - read_cached_torrent_by_info_hash(app.handle(), &parsed.info_hash) + read_cached_torrent_by_info_hash_at(&root, &parsed.info_hash) .await .expect("invalid cache should be handled") .is_none() @@ -42,21 +38,19 @@ async fn canonical_cache_round_trip_rejects_invalid_bytes_and_source_metadata() let tracker_hash = parse_torrent_bytes(tracker_bytes) .expect("tracker-bearing torrent should parse") .info_hash; - let tracker_path = managed_torrent_info_hash_path(app.handle(), &tracker_hash) - .expect("tracker cache path should resolve"); - let _ = tokio::fs::remove_file(&tracker_path).await; - assert!(cache_torrent_info_hash(app.handle(), tracker_bytes) + let tracker_path = cache_torrent_info_hash_at(&root, tracker_bytes) .await .expect("tracker metadata should be reusable") - .is_some()); + .expect("tracker cache path should be returned"); + let tracker_path = std::path::PathBuf::from(tracker_path); assert_eq!( - read_cached_torrent_by_info_hash(app.handle(), &tracker_hash) + read_cached_torrent_by_info_hash_at(&root, &tracker_hash) .await .expect("tracker cache should be readable"), Some(tracker_bytes.to_vec()) ); - assert!(cache_torrent_info_hash( - app.handle(), + assert!(cache_torrent_info_hash_at( + &root, b"d4:infod6:lengthi5e4:name4:teste8:url-list22:https://example.test/ae" ) .await