Files
lanspread/crates/lanspread-compat/tests/catalog_publisher_cli.rs
ddidderr 60fd7ba0c2 feat(peer)!: cut over to authenticated catalog sharing
Replace address-only trust and pushed peer state with installation identities,
SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned
protocol-8 pulls. The runtime now owns each network generation and all admitted
work through shutdown.

Add exact bundled content identities, reproducible manifest publishing,
capability-confined downloads, streaming BLAKE3 verification, quarantine and
retry, and crash-recoverable download and install transactions. Ship generated
fixture catalogs and fail closed when production manifests are absent.

The Tauri backend exposes durable sharing policy, redacted identity state, and
attempt-keyed transfer snapshots. Frontend consumption follows in the next
commit. Repository-wide test certificates and protocol-7 paths are removed.

BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts;
protocol-7 frames and shared-certificate identities are no longer accepted.

Test Plan:
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `just build` -- passed with fixture catalogs on the completed stack
- `just catalog-check-production` -- failed closed because the external
  production manifest corpus is absent
- `git diff --cached --check` -- passed
2026-08-10 13:59:18 +02:00

180 lines
5.3 KiB
Rust

use std::{
fs,
path::{Path, PathBuf},
process::Command,
sync::atomic::{AtomicU64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use lanspread_compat::catalog_publisher::cli::HELP;
static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should follow epoch")
.as_nanos();
let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"lanspread-publisher-cli-{}-{nanos}-{sequence}",
std::process::id()
));
fs::create_dir(&path).expect("temporary directory should be created");
Self(path)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn publisher() -> Command {
Command::new(env!("CARGO_BIN_EXE_lanspread-catalog-publisher"))
}
fn catalog_db() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../lanspread-tauri-deno-ts/src-tauri/game.db")
}
fn packages_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../lanspread-peer-cli/fixtures/fixture-persona")
}
fn test_unrar() -> Option<PathBuf> {
#[cfg(target_os = "linux")]
{
let bundled = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../lanspread-tauri-deno-ts/src-tauri/binaries/unrar-x86_64-unknown-linux-gnu");
if bundled.is_file() {
return Some(bundled);
}
}
["/usr/local/bin/unrar", "/usr/bin/unrar"]
.into_iter()
.map(PathBuf::from)
.find(|path| path.is_file())
}
async fn create_single_game_catalog(path: &Path) {
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(
SqliteConnectOptions::new()
.filename(path)
.create_if_missing(true),
)
.await
.expect("test catalog should open");
sqlx::query(
"CREATE TABLE games (game_id TEXT NOT NULL, game_version TEXT NOT NULL, db_id INTEGER NOT NULL)",
)
.execute(&pool)
.await
.expect("games table should be created");
sqlx::query("INSERT INTO games (game_id, game_version, db_id) VALUES ('css', '20240623', 1)")
.execute(&pool)
.await
.expect("css row should be inserted");
pool.close().await;
}
#[test]
fn help_is_exact_and_error_output_is_separate() {
let help = publisher()
.arg("--help")
.output()
.expect("publisher should run");
assert!(help.status.success());
assert_eq!(
String::from_utf8(help.stdout).expect("UTF-8 stdout"),
format!("{HELP}\n")
);
assert!(help.stderr.is_empty());
let error = publisher()
.args(["check", "--catalog-db"])
.arg(catalog_db())
.output()
.expect("publisher should run");
assert!(!error.status.success());
assert!(error.stdout.is_empty());
assert_eq!(
String::from_utf8(error.stderr).expect("UTF-8 stderr"),
"error: select games with --all or at least one --game-id\n"
);
}
#[tokio::test]
async fn complete_real_package_round_trips_through_generate_and_check_commands() {
let Some(unrar) = test_unrar() else {
return;
};
let temp = TempDir::new();
let fixture_catalog = temp.0.join("game.db");
create_single_game_catalog(&fixture_catalog).await;
let manifests = temp.0.join("manifests");
let generated = publisher()
.arg("generate")
.arg("--catalog-db")
.arg(&fixture_catalog)
.arg("--packages-dir")
.arg(packages_dir())
.arg("--manifests-dir")
.arg(&manifests)
.arg("--unrar")
.arg(unrar)
.arg("--all")
.output()
.expect("publisher should run");
assert!(
generated.status.success(),
"generation failed: {}",
String::from_utf8_lossy(&generated.stderr)
);
assert!(generated.stderr.is_empty());
let generated = String::from_utf8(generated.stdout).expect("UTF-8 stdout");
let generated_lines = generated.lines().collect::<Vec<_>>();
assert_eq!(generated_lines.len(), 2);
let prefix = "generated game_id=css game_version=20240623 content_id=";
let content_id = generated_lines[0]
.strip_prefix(prefix)
.expect("stable generated record prefix");
assert_eq!(content_id.len(), 64);
assert!(
content_id
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
);
assert_eq!(generated_lines[1], "generated total=1");
let checked = publisher()
.arg("check")
.arg("--catalog-db")
.arg(fixture_catalog)
.arg("--manifests-dir")
.arg(manifests)
.args(["--game-id", "css"])
.output()
.expect("publisher should run");
assert!(
checked.status.success(),
"check failed: {}",
String::from_utf8_lossy(&checked.stderr)
);
assert!(checked.stderr.is_empty());
assert_eq!(
String::from_utf8(checked.stdout).expect("UTF-8 stdout"),
format!(
"checked game_id=css game_version=20240623 content_id={content_id}\nchecked total=1\n"
)
);
}