testing: add client layout/paste and common cli contracts
This commit is contained in:
parent
b6187bdf85
commit
2b27b1d72f
@ -14,3 +14,7 @@ libc = "0.2"
|
||||
lesavka_client = { path = "../client" }
|
||||
lesavka_common = { path = "../common" }
|
||||
lesavka_server = { path = "../server" }
|
||||
chacha20poly1305 = "0.10"
|
||||
serial_test = { workspace = true }
|
||||
temp-env = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
99
testing/tests/client_layout_contract.rs
Normal file
99
testing/tests/client_layout_contract.rs
Normal file
@ -0,0 +1,99 @@
|
||||
//! Integration coverage for the client window layout contract.
|
||||
//!
|
||||
//! Scope: exercise `layout::apply` end-to-end against a fake `swaymsg` binary.
|
||||
//! Targets: `client/src/layout.rs`.
|
||||
//! Why: this keeps layout policy coverage centralized in `testing/tests` while
|
||||
//! avoiding source-LOC ratchet growth in the client crate.
|
||||
|
||||
use lesavka_client::layout::{Layout, apply};
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use temp_env::with_var;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn install_sway_stub(dir: &std::path::Path) -> std::path::PathBuf {
|
||||
let sway_path = dir.join("swaymsg");
|
||||
fs::write(
|
||||
&sway_path,
|
||||
r#"#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "${1:-}" == "-t" && "${2:-}" == "get_outputs" && "${3:-}" == "-r" ]]; then
|
||||
printf '%s\n' "${LESAVKA_TEST_SWAY_OUTPUTS:-[]}"
|
||||
exit 0
|
||||
fi
|
||||
echo "$*" >> "${LESAVKA_TEST_SWAY_LOG}"
|
||||
"#,
|
||||
)
|
||||
.expect("write swaymsg stub");
|
||||
let mut perms = fs::metadata(&sway_path)
|
||||
.expect("read swaymsg stub metadata")
|
||||
.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&sway_path, perms).expect("mark swaymsg stub executable");
|
||||
sway_path
|
||||
}
|
||||
|
||||
fn run_apply_with_outputs(layout: Layout, outputs_json: &str) -> Option<String> {
|
||||
let dir = tempdir().expect("create temp dir");
|
||||
install_sway_stub(dir.path());
|
||||
let log_path = dir.path().join("sway.log");
|
||||
let old_path = std::env::var("PATH").unwrap_or_default();
|
||||
let new_path = format!("{}:{old_path}", dir.path().display());
|
||||
let log_value = log_path.to_string_lossy().to_string();
|
||||
|
||||
with_var("PATH", Some(new_path.as_str()), || {
|
||||
with_var("LESAVKA_TEST_SWAY_LOG", Some(log_value.as_str()), || {
|
||||
with_var("LESAVKA_TEST_SWAY_OUTPUTS", Some(outputs_json), || {
|
||||
apply(layout);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fs::read_to_string(log_path).ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn side_by_side_places_each_eye_on_half_of_focused_output() {
|
||||
let outputs = r#"[{"focused":true,"rect":{"x":10,"y":20,"width":1920,"height":1080}}]"#;
|
||||
let log = run_apply_with_outputs(Layout::SideBySide, outputs).expect("read command log");
|
||||
assert!(log.contains(r#"[title="^Lesavka-eye-0$"] resize set 960 1080; move position 10 20"#));
|
||||
assert!(log.contains(r#"[title="^Lesavka-eye-1$"] resize set 960 1080; move position 970 20"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn full_layout_modes_hide_the_opposite_eye_offscreen() {
|
||||
let outputs = r#"[{"focused":true,"rect":{"x":100,"y":50,"width":640,"height":360}}]"#;
|
||||
let left = run_apply_with_outputs(Layout::FullLeft, outputs).expect("left layout log");
|
||||
assert!(left.contains(r#"[title="^Lesavka-eye-0$"] resize set 640 360; move position 100 50"#));
|
||||
assert!(left.contains(r#"[title="^Lesavka-eye-1$"] resize set 1 1; move position 1380 770"#));
|
||||
|
||||
let right = run_apply_with_outputs(Layout::FullRight, outputs).expect("right layout log");
|
||||
assert!(
|
||||
right.contains(r#"[title="^Lesavka-eye-1$"] resize set 640 360; move position 100 50"#)
|
||||
);
|
||||
assert!(right.contains(r#"[title="^Lesavka-eye-0$"] resize set 1 1; move position 1380 770"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn apply_skips_window_commands_when_outputs_payload_is_invalid() {
|
||||
let log = run_apply_with_outputs(Layout::SideBySide, "{not-json").unwrap_or_default();
|
||||
assert!(
|
||||
log.trim().is_empty(),
|
||||
"expected no placement calls, got: {log}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn apply_skips_window_commands_when_no_output_is_focused() {
|
||||
let outputs = r#"[{"focused":false,"rect":{"x":0,"y":0,"width":1920,"height":1080}}]"#;
|
||||
let log = run_apply_with_outputs(Layout::SideBySide, outputs).unwrap_or_default();
|
||||
assert!(
|
||||
log.trim().is_empty(),
|
||||
"expected no placement calls, got: {log}"
|
||||
);
|
||||
}
|
||||
52
testing/tests/client_paste_contract.rs
Normal file
52
testing/tests/client_paste_contract.rs
Normal file
@ -0,0 +1,52 @@
|
||||
//! Integration coverage for the client paste-request contract.
|
||||
//!
|
||||
//! Scope: validate key loading, encryption metadata, and truncation behavior
|
||||
//! through the public paste helper.
|
||||
//! Targets: `client/src/paste.rs`.
|
||||
//! Why: these checks are pure policy and crypto framing, so they belong in the
|
||||
//! centralized `testing/tests` contract suite.
|
||||
|
||||
use chacha20poly1305::aead::Aead;
|
||||
use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce};
|
||||
use lesavka_client::paste::build_paste_request;
|
||||
use serial_test::serial;
|
||||
use temp_env::with_var;
|
||||
|
||||
const TEST_KEY_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn build_paste_request_requires_a_shared_key() {
|
||||
with_var("LESAVKA_PASTE_KEY", None::<&str>, || {
|
||||
let err = build_paste_request("hello").expect_err("missing key should fail");
|
||||
assert!(format!("{err:#}").contains("LESAVKA_PASTE_KEY"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn build_paste_request_sets_encryption_fields() {
|
||||
with_var("LESAVKA_PASTE_KEY", Some(TEST_KEY_HEX), || {
|
||||
let req = build_paste_request("hello world").expect("build request");
|
||||
assert!(req.encrypted);
|
||||
assert_eq!(req.nonce.len(), 12);
|
||||
assert!(!req.data.is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn build_paste_request_truncates_plaintext_before_encryption() {
|
||||
with_var("LESAVKA_PASTE_KEY", Some(TEST_KEY_HEX), || {
|
||||
with_var("LESAVKA_PASTE_MAX", Some("5"), || {
|
||||
let req = build_paste_request("hello-from-lesavka").expect("build truncated request");
|
||||
let key = lesavka_common::paste::decode_shared_key(TEST_KEY_HEX).expect("decode key");
|
||||
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key));
|
||||
let nonce = Nonce::from_slice(&req.nonce);
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, req.data.as_ref())
|
||||
.expect("decrypt request data");
|
||||
assert_eq!(std::str::from_utf8(&plaintext).expect("utf8"), "hello");
|
||||
});
|
||||
});
|
||||
}
|
||||
12
testing/tests/common_cli_contract.rs
Normal file
12
testing/tests/common_cli_contract.rs
Normal file
@ -0,0 +1,12 @@
|
||||
//! Integration coverage for the common CLI entrypoint contract.
|
||||
//!
|
||||
//! Scope: execute the public common CLI helper from the centralized testing
|
||||
//! crate.
|
||||
//! Targets: `common/src/lib.rs`.
|
||||
//! Why: this keeps even tiny user-facing helpers represented in cross-crate
|
||||
//! contract coverage without package-local integration tests.
|
||||
|
||||
#[test]
|
||||
fn run_cli_executes_without_panicking() {
|
||||
lesavka_common::run_cli();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user