test(peer): cover serve gating dispatch
Add focused serve-side tests for the gates around peer requests. GetGame now has coverage for the non-catalog, active-operation, and missing-sentinel cases that should return GameNotFound instead of exposing local files. The full-file and chunk handlers both depend on the same transfer gate before touching the QUIC send stream. Extract that gate into a small helper and test the same cases there, plus the existing local-path exclusion, so both dispatch paths stay aligned without adding fake QUIC stream plumbing. Test Plan: - git diff --check - just fmt - just clippy - just test Follow-up-Plan: FOLLOW_UP_2.md
This commit is contained in:
@@ -296,6 +296,16 @@ async fn can_serve_game(ctx: &PeerCtx, game_dir: &std::path::Path, game_id: &str
|
||||
local_download_available(game_dir, game_id, &active_operations, &catalog).await
|
||||
}
|
||||
|
||||
async fn can_dispatch_file_transfer(
|
||||
ctx: &PeerCtx,
|
||||
game_dir: &std::path::Path,
|
||||
game_id: &str,
|
||||
relative_path: &str,
|
||||
) -> bool {
|
||||
!path_points_inside_local(game_id, relative_path)
|
||||
&& can_serve_game(ctx, game_dir, game_id).await
|
||||
}
|
||||
|
||||
fn path_points_inside_local(game_id: &str, relative_path: &str) -> bool {
|
||||
let normalised = relative_path.replace('\\', "/");
|
||||
let mut parts = normalised.split('/').filter(|part| !part.is_empty());
|
||||
@@ -318,9 +328,7 @@ async fn handle_file_data_request(
|
||||
|
||||
let mut tx = framed_tx.into_inner();
|
||||
let game_dir = ctx.game_dir.read().await.clone();
|
||||
if path_points_inside_local(&desc.game_id, &desc.relative_path)
|
||||
|| !can_serve_game(ctx, &game_dir, &desc.game_id).await
|
||||
{
|
||||
if !can_dispatch_file_transfer(ctx, &game_dir, &desc.game_id, &desc.relative_path).await {
|
||||
log::info!(
|
||||
"Declining GetGameFileData for {} because the game is not currently transferable",
|
||||
desc.relative_path
|
||||
@@ -347,9 +355,7 @@ async fn handle_file_chunk_request(
|
||||
|
||||
let mut tx = framed_tx.into_inner();
|
||||
let game_dir = ctx.game_dir.read().await.clone();
|
||||
if path_points_inside_local(&game_id, &relative_path)
|
||||
|| !can_serve_game(ctx, &game_dir, &game_id).await
|
||||
{
|
||||
if !can_dispatch_file_transfer(ctx, &game_dir, &game_id, &relative_path).await {
|
||||
log::info!(
|
||||
"Declining GetGameFileChunk for {relative_path} because the game is not currently transferable"
|
||||
);
|
||||
@@ -389,7 +395,87 @@ async fn handle_announce_games(ctx: &PeerCtx, remote_addr: Option<SocketAddr>, g
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::path_points_inside_local;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
UnpackFuture,
|
||||
Unpacker,
|
||||
context::{Ctx, OperationKind},
|
||||
peer_db::PeerGameDB,
|
||||
};
|
||||
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> Self {
|
||||
let mut path = std::env::temp_dir();
|
||||
let unique_id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
path.push(format!(
|
||||
"lanspread-stream-{}-{}-{}",
|
||||
std::process::id(),
|
||||
unique_id,
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&path).expect("temp dir should be created");
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
struct NoopUnpacker;
|
||||
|
||||
impl Unpacker for NoopUnpacker {
|
||||
fn unpack<'a>(&'a self, _archive: &'a Path, _dest: &'a Path) -> UnpackFuture<'a> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
}
|
||||
|
||||
fn write_file(path: &Path, bytes: &[u8]) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("parent dir should be created");
|
||||
}
|
||||
std::fs::write(path, bytes).expect("file should be written");
|
||||
}
|
||||
|
||||
fn test_ctx(game_dir: PathBuf, catalog: HashSet<String>) -> PeerCtx {
|
||||
let (tx_notify_ui, _rx) = mpsc::unbounded_channel();
|
||||
Ctx::new(
|
||||
Arc::new(RwLock::new(PeerGameDB::new())),
|
||||
"peer".to_string(),
|
||||
game_dir,
|
||||
Arc::new(NoopUnpacker),
|
||||
CancellationToken::new(),
|
||||
TaskTracker::new(),
|
||||
Arc::new(RwLock::new(catalog)),
|
||||
)
|
||||
.to_peer_ctx(tx_notify_ui)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_relative_paths_are_never_transferable() {
|
||||
@@ -399,4 +485,99 @@ mod tests {
|
||||
assert!(!path_points_inside_local("game", "game/version.ini"));
|
||||
assert!(!path_points_inside_local("game", "game/archive.eti"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_game_response_respects_serve_gates() {
|
||||
let temp = TempDir::new();
|
||||
write_file(&temp.path().join("ready").join("version.ini"), b"20250101");
|
||||
write_file(
|
||||
&temp.path().join("non-catalog").join("version.ini"),
|
||||
b"20250101",
|
||||
);
|
||||
write_file(&temp.path().join("active").join("version.ini"), b"20250101");
|
||||
std::fs::create_dir_all(temp.path().join("missing-sentinel"))
|
||||
.expect("missing sentinel root should be created");
|
||||
|
||||
let ctx = test_ctx(
|
||||
temp.path().to_path_buf(),
|
||||
HashSet::from([
|
||||
"ready".to_string(),
|
||||
"active".to_string(),
|
||||
"missing-sentinel".to_string(),
|
||||
]),
|
||||
);
|
||||
ctx.active_operations
|
||||
.write()
|
||||
.await
|
||||
.insert("active".to_string(), OperationKind::Downloading);
|
||||
|
||||
assert!(matches!(
|
||||
get_game_response(&ctx, "ready".to_string()).await,
|
||||
Response::GetGame { id, .. } if id == "ready"
|
||||
));
|
||||
assert!(matches!(
|
||||
get_game_response(&ctx, "non-catalog".to_string()).await,
|
||||
Response::GameNotFound(id) if id == "non-catalog"
|
||||
));
|
||||
assert!(matches!(
|
||||
get_game_response(&ctx, "active".to_string()).await,
|
||||
Response::GameNotFound(id) if id == "active"
|
||||
));
|
||||
assert!(matches!(
|
||||
get_game_response(&ctx, "missing-sentinel".to_string()).await,
|
||||
Response::GameNotFound(id) if id == "missing-sentinel"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_transfer_dispatch_respects_serve_gates() {
|
||||
let temp = TempDir::new();
|
||||
write_file(&temp.path().join("ready").join("version.ini"), b"20250101");
|
||||
write_file(
|
||||
&temp.path().join("non-catalog").join("version.ini"),
|
||||
b"20250101",
|
||||
);
|
||||
write_file(&temp.path().join("active").join("version.ini"), b"20250101");
|
||||
std::fs::create_dir_all(temp.path().join("missing-sentinel"))
|
||||
.expect("missing sentinel root should be created");
|
||||
|
||||
let ctx = test_ctx(
|
||||
temp.path().to_path_buf(),
|
||||
HashSet::from([
|
||||
"ready".to_string(),
|
||||
"active".to_string(),
|
||||
"missing-sentinel".to_string(),
|
||||
]),
|
||||
);
|
||||
ctx.active_operations
|
||||
.write()
|
||||
.await
|
||||
.insert("active".to_string(), OperationKind::Downloading);
|
||||
|
||||
assert!(can_dispatch_file_transfer(&ctx, temp.path(), "ready", "ready/version.ini").await);
|
||||
assert!(
|
||||
!can_dispatch_file_transfer(
|
||||
&ctx,
|
||||
temp.path(),
|
||||
"non-catalog",
|
||||
"non-catalog/version.ini",
|
||||
)
|
||||
.await
|
||||
);
|
||||
assert!(
|
||||
!can_dispatch_file_transfer(&ctx, temp.path(), "active", "active/version.ini").await
|
||||
);
|
||||
assert!(
|
||||
!can_dispatch_file_transfer(
|
||||
&ctx,
|
||||
temp.path(),
|
||||
"missing-sentinel",
|
||||
"missing-sentinel/archive.eti",
|
||||
)
|
||||
.await
|
||||
);
|
||||
assert!(
|
||||
!can_dispatch_file_transfer(&ctx, temp.path(), "ready", "ready/local/save.dat").await
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user