//! The wave-2 dispatch/decline-core contract tests //! (`w2-dispatch-decline-core`) — the skeleton rows of the decline //! matrix (`mapping.md` §Decline matrix), driven per feature build //! through the loopback harness. This task tests the rows it lands; //! the full per-build matrix suite is wave-5's. //! //! Feature builds are compile-time, so the rows are feature-gated to //! the build whose matrix row they pin: the shared rows //! (never-feature `x11`, the `tcpip-forward` listen decline) run in //! every build; the capability rows run in the builds that open their //! subject (a session channel needs `session` or `git`); the //! per-build matrix rows are pinned to exactly one feature combination //! (`not(...)` guards) so `--all-features` runs neither a //! git-only row nor a session-only row by accident. Every feature //! build is exercised by its own verification run //! (`cargo test [--features …]`), not by cfg tricks. #![allow(dead_code)] mod common; use std::sync::Arc; use common::harness::{ LoopbackSession, adapter, authenticate_publickey, await_close, drive, next_message, open_session_channel, open_x11_channel, tcpip_forward, }; use common::{MemHostKeyPolicy, MemIdentityProvider, fixture, identity}; use russh::client::Msg; use russh::{Channel, ChannelOpenFailure, Error}; use alkssh::Limits; /// Mounts an authenticated loopback session over the doubles — the /// dispatch rows' driver: fixture `alpha` resolves through /// `MemIdentityProvider`, host keys `bravo`, the permissive host-key /// posture (the explicit test choice). async fn authenticated() -> LoopbackSession { let provider = Arc::new(MemIdentityProvider::new()); provider.register( fixture("alpha").fingerprint, identity("dispatch-core", &["ssh"]), ); let adapter = adapter(provider, &[fixture("bravo")]).await; let policy = Arc::new(MemHostKeyPolicy::with_default(true)); let mut session = drive(adapter, Arc::clone(&policy), Limits::default()) .await .expect("the loopback mounts"); let result = authenticate_publickey(&mut session.client, "dispatch-core", fixture("alpha")).await; assert!( result.success(), "the fixture identity authenticates before every dispatch row" ); session } /// Matches an open failure's SSH reason — the wire decline reason the /// open-gate rows assert. fn open_failure(err: Error, expected: ChannelOpenFailure, context: &str) { assert!( matches!( &err, Error::ChannelOpenFailure(reason) if *reason == expected ), "{context}: expected open decline with {expected:?}, got {err:?}" ); } /// The request-level decline observation: the channel answers Failure. /// A capability decline's channel stays open afterwards — the POC-1 §5 /// session-survives-declines posture the channel-alive rows lean on. async fn expect_failure(channel: &mut Channel, context: &str) { match next_message(channel).await { Some(russh::ChannelMsg::Failure) => {} other => panic!("{context}: expected a channel failure, got {other:?}"), } } /// The serving-path (family-seam stub) decline observation: Failure /// then Close — the vanilla-clean shape for a row whose serving path /// would own the channel. async fn expect_failure_then_close(channel: &mut Channel, context: &str) { expect_failure(channel, context).await; assert!( await_close(channel).await, "{context}: the seam stub closes the channel after the failure" ); } /// Proves a declined channel is still usable — a follow-up request /// reaches the dispatch again instead of the channel being dead. async fn expect_channel_alive(channel: &mut Channel) { common::harness::request_subsystem(channel, "sftp") .await .expect("the still-open channel carries another request"); expect_failure(channel, "the surviving channel still dispatches").await; } // --------------------------------------------------------------------------- // Shared rows — run in every feature build. // --------------------------------------------------------------------------- /// - *Contract stamp:* ADR-003 §5 (x11 is a never-feature — declined /// in every build, never feature-gated). #[tokio::test] async fn x11_channel_opens_decline_in_every_build() { let session = authenticated().await; let err = open_x11_channel(&session.client, "127.0.0.1", 0) .await .expect_err("x11 opens decline"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "the never-feature x11 open", ); session.unmount().await; } /// - *Contract stamp:* mapping.md §Global-request rows (the `-R` /// listen flow is a `tunnel` row declined when the flow cannot /// serve — this wave: every build). #[tokio::test] async fn tcpip_forward_listen_requests_decline_in_every_build() { let session = authenticated().await; let err = tcpip_forward(&session.client, "127.0.0.1", 8080) .await .expect_err("the listen request declines"); assert!( matches!(err, Error::RequestDenied), "expected the declined listen's RequestDenied, got {err:?}" ); session.unmount().await; } /// - *Contract stamp:* mapping.md §Decline matrix (git-build row, /// request side: `x11` / `auth-agent` requests decline; ADR-003 §5). #[cfg(any(feature = "git", feature = "session"))] #[tokio::test] async fn x11_and_agent_requests_decline_wherever_sessions_open() { use common::harness::{request_agent_forward, request_x11}; let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens where a session-serving feature exists"); request_x11(&channel) .await .expect("the x11-req rides the wire"); expect_failure(&mut channel, "the never-feature x11 request declines").await; request_agent_forward(&channel) .await .expect("the agent-forward request rides the wire"); expect_failure(&mut channel, "the never-feature agent request declines").await; expect_channel_alive(&mut channel).await; session.unmount().await; } // --------------------------------------------------------------------------- // Default build (no features): everything declines (mapping.md // §Decline matrix — "none (default): declines everything"). // --------------------------------------------------------------------------- #[cfg(not(any(feature = "session", feature = "git", feature = "tunnel")))] mod default_build { use super::*; use common::harness::{open_direct_streamlocal, open_direct_tcpip}; /// - *Contract stamp:* mapping.md §Decline matrix (none-build /// row: every channel type declines at open — session channels /// as unfeatured, tunnel rows as unfeatured — plus the /// never-feature x11). #[tokio::test] async fn every_channel_open_declines_in_a_default_build() { let session = authenticated().await; let err = open_session_channel(&session.client) .await .expect_err("session channels decline unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "the unfeatured session open", ); let err = open_direct_tcpip(&session.client, "internal.example", 5432) .await .expect_err("direct-tcpip declines unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "the unfeatured direct-tcpip open", ); let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock") .await .expect_err("direct-streamlocal declines unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "the unfeatured direct-streamlocal open", ); session.unmount().await; } } // --------------------------------------------------------------------------- // git-only build: routes git-parseable exec to the git row; declines // pty/shell, plain exec, all other opens (mapping.md §Decline matrix — // git row). The harness builds no seam, so the git row's decline is // the config-driven one (`w3-git-mapping`: seam-unconfigured — the // unset-declines contract, serving-path shape); the serving rows ride // `tests/git_mapping.rs`. // --------------------------------------------------------------------------- #[cfg(all(feature = "git", not(feature = "session"), not(feature = "tunnel")))] mod git_build { use super::*; use common::harness::{ open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_shell, request_subsystem, }; /// - *Contract stamp:* the task's channel-open gate (session /// channels open where a session-serving feature exists — `git` /// qualifies, the parse path lives inside session channels). #[tokio::test] async fn session_channels_open_in_a_git_build() { let session = authenticated().await; open_session_channel(&session.client) .await .expect("session channels open under the git feature"); session.unmount().await; } /// - *Contract stamp:* ADR-006 point 3 (parse-only: a /// git-parseable exec routes to the git row; this build /// configures no seam, so the row declines at its config gate — /// the serving-path shape, failure then close). The row's /// serving surface (seam configured, real dispatch) is /// `tests/git_mapping.rs`'s. #[tokio::test] async fn git_parseable_exec_routes_to_the_git_row_stub() { let session = authenticated().await; for command in [ "git-upload-pack '/owner/demo'", "git-receive-pack '/owner/demo'", ] { let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, command) .await .expect("the exec request rides the wire"); expect_failure_then_close( &mut channel, &format!("the git row serves its seam stub for {command}"), ) .await; } session.unmount().await; } /// - *Contract stamp:* mapping.md §Decline matrix (git-build row: /// plain exec declines; the parse-then-fail shape of POC-3 /// stage A — the channel survives the decline). #[tokio::test] async fn plain_exec_declines_and_keeps_the_channel_in_a_git_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, "uptime") .await .expect("the exec request rides the wire"); expect_failure(&mut channel, "the unfeatured pipe row declines").await; expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* POC-3 §2 (unknown git services take the /// agreed declined surface — `w3-git-mapping`'s decided class, /// off the pipe row: `ExecRoute::Declined`, build-independent; /// the reply vocabulary is shared with parse-then-fail: failure, /// channel alive). #[tokio::test] async fn unknown_git_services_take_the_declined_surface() { let session = authenticated().await; for command in ["git-send-pack '/r'", "git-upload-archive '/r'"] { let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, command) .await .expect("the exec request rides the wire"); expect_failure( &mut channel, &format!("an unknown service does not reach the git row: {command}"), ) .await; expect_channel_alive(&mut channel).await; } session.unmount().await; } /// - *Contract stamp:* mapping.md §Decline matrix (git-build row: /// pty/shell and all session requests decline). #[tokio::test] async fn pty_shell_and_capability_requests_decline_in_a_git_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_shell(&channel) .await .expect("the shell request rides the wire"); expect_failure(&mut channel, "the shell row declines unfeatured").await; expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* mapping.md §Request-level rows (subsystem: /// `sftp` is the OQ-13 placeholder decline; git service names /// carry no repository id — ADR-006 decision 2). #[tokio::test] async fn subsystems_decline_with_their_named_reasons_in_a_git_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); for name in ["sftp", "git-upload-pack"] { request_subsystem(&channel, name) .await .expect("the subsystem request rides the wire"); expect_failure( &mut channel, &format!("the subsystem {name} declines with reason"), ) .await; } expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* OQ-09 (mapping.md §Request-level rows: /// `env_request` declines unconditionally — no allowlist ships). #[tokio::test] async fn env_requests_decline_in_a_git_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_env(&channel, "GIT_DIR", "/srv/git") .await .expect("the env request rides the wire"); expect_failure(&mut channel, "the env request declines").await; expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* mapping.md §Decline matrix (git-build /// row: all other channel types decline — tunnel unfeatured). #[tokio::test] async fn tunnel_opens_decline_unfeatured_in_a_git_build() { let session = authenticated().await; let err = open_direct_tcpip(&session.client, "internal.example", 5432) .await .expect_err("direct-tcpip declines unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "direct-tcpip", ); let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock") .await .expect_err("direct-streamlocal declines unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "direct-streamlocal", ); session.unmount().await; } } // --------------------------------------------------------------------------- // session-only build: declines git-parseable exec (the git parse is // its gate); with no seam/config this build's session rows decline at // their config gates (the unset-declines contract, serving-path // shape: `w3-tty-session`); declines tunnel opens (mapping.md // §Decline matrix — session row). // --------------------------------------------------------------------------- #[cfg(all(feature = "session", not(feature = "git"), not(feature = "tunnel")))] mod session_build { use super::*; use common::harness::{ open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_pty, request_shell, request_subsystem, }; /// - *Contract stamp:* the task's channel-open gate (session /// channels open where a session-serving feature exists). #[tokio::test] async fn session_channels_open_in_a_session_build() { let session = authenticated().await; open_session_channel(&session.client) .await .expect("session channels open under the session feature"); session.unmount().await; } /// - *Contract stamp:* mapping.md §Decline matrix (session row: /// git-parseable exec declines — the `git` parse is its gate; /// the channel survives the decline, POC-1 §5). #[tokio::test] async fn git_parseable_exec_declines_in_a_session_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, "git-upload-pack '/owner/demo'") .await .expect("the exec request rides the wire"); expect_failure( &mut channel, "the git parse gates the row off in a session build", ) .await; expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* OQ-08 + the recorded wire shape /// (`w3-tty-session`: `exec_wrapper_argv` unset → the pipe row /// declines — the serving-path shape, failure then close, never /// a fallback). The row's serving surface (everything /// configured) is `tests/tty_mapping.rs`'s. #[tokio::test] async fn plain_exec_declines_wrapper_unconfigured_in_a_session_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, "uptime") .await .expect("the exec request rides the wire"); expect_failure_then_close(&mut channel, "the unset-wrapper pipe row").await; session.unmount().await; } /// - *Contract stamp:* mapping.md §Channel-type rows + OQ-08 (the /// `session` feature alone configures nothing: `session_seam` /// unset → the pty/shell rows decline `seam-unconfigured`, the /// serving-path shape this build pins). #[tokio::test] async fn pty_and_shell_decline_seam_unconfigured_in_a_session_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_pty(&channel) .await .expect("the pty request rides the wire"); expect_failure_then_close(&mut channel, "the unset-seam pty row").await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_shell(&channel) .await .expect("the shell request rides the wire"); expect_failure_then_close(&mut channel, "the unset-seam shell row").await; session.unmount().await; } /// - *Contract stamp:* mapping.md §Request-level rows (subsystem: /// the OQ-13 placeholder and git service names decline-with- /// reason in every session-serving build). #[tokio::test] async fn subsystems_decline_with_their_named_reasons_in_a_session_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); for name in ["sftp", "git-upload-pack"] { request_subsystem(&channel, name) .await .expect("the subsystem request rides the wire"); expect_failure( &mut channel, &format!("the subsystem {name} declines with reason"), ) .await; } expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* OQ-09 (mapping.md §Request-level rows: /// `env_request` declines unconditionally). #[tokio::test] async fn env_requests_decline_in_a_session_build() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_env(&channel, "GIT_DIR", "/srv/git") .await .expect("the env request rides the wire"); expect_failure(&mut channel, "the env request declines").await; expect_channel_alive(&mut channel).await; session.unmount().await; } /// - *Contract stamp:* mapping.md §Decline matrix (session row: /// other channel types decline — tunnel unfeatured). #[tokio::test] async fn tunnel_opens_decline_unfeatured_in_a_session_build() { let session = authenticated().await; let err = open_direct_tcpip(&session.client, "internal.example", 5432) .await .expect_err("direct-tcpip declines unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "direct-tcpip", ); let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock") .await .expect_err("direct-streamlocal declines unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "direct-streamlocal", ); session.unmount().await; } } // --------------------------------------------------------------------------- // tunnel-only build: session channels decline (auto-reject); the // tunnel direct rows decline on the **absent handle** even with the // feature on (the route-1 bridge is `w3-tunnel-direct`: the pre-admit // unset-handle class — the unset-declines posture; the fabric double's // serve/cap/fabric-failure rows ride tests/tunnel_mapping.rs) — // mapping.md §Decline matrix (tunnel row). // --------------------------------------------------------------------------- #[cfg(all(feature = "tunnel", not(feature = "git"), not(feature = "session")))] mod tunnel_build { use super::*; use common::harness::{open_direct_streamlocal, open_direct_tcpip}; /// - *Contract stamp:* ADR-006 point 3 as amended (parse-only /// dispatch: a request-less/deserving-less session channel /// declines as unfeatured — a tunnel-only build has no /// session-serving feature, so the open declines). #[tokio::test] async fn session_channels_decline_in_a_tunnel_build() { let session = authenticated().await; let err = open_session_channel(&session.client) .await .expect_err("session channels decline unfeatured"); open_failure( err, ChannelOpenFailure::AdministrativelyProhibited, "the unfeatured session open in a tunnel build", ); session.unmount().await; } /// - *Contract stamp:* mapping.md §Channel-type rows + the /// unset-declines posture (`w3-seam-config` — the feature alone /// configures nothing): the direct rows decline on the **absent /// fabric handle** even with `tunnel` on — /// [`connect failed`], the cannot-bridge class ADR-006 decision /// 1 pins (`w3-tunnel-direct`'s recorded wire classes: policy /// refusals are administratively-prohibited, cannot-bridge is /// connect-failed). #[tokio::test] async fn tunnel_direct_rows_decline_seam_unconfigured_without_a_handle() { let session = authenticated().await; let err = open_direct_tcpip(&session.client, "internal.example", 5432) .await .expect_err("the direct-tcpip row declines on the absent handle"); open_failure( err, ChannelOpenFailure::ConnectFailed, "the direct-tcpip unset-handle decline", ); let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock") .await .expect_err("the direct-streamlocal row declines on the absent handle"); open_failure( err, ChannelOpenFailure::ConnectFailed, "the direct-streamlocal unset-handle decline", ); session.unmount().await; } } // --------------------------------------------------------------------------- // All features: the routing decisions hold with the full graph on — // the git row is still its config-driven decline in this binary (the // serving rows ride tests/git_mapping.rs), and the session rows are // still their config-driven declines (the serving rows ride // tests/tty_mapping.rs). // --------------------------------------------------------------------------- #[cfg(all(feature = "git", feature = "session", feature = "tunnel"))] mod all_features { use super::*; use common::harness::{ open_direct_streamlocal, open_direct_tcpip, request_env, request_exec, request_pty, request_shell, request_subsystem, }; /// - *Contract stamp:* mapping.md §Session-channel dispatch (the /// git-parseable exec routes to the git row in the full build). #[tokio::test] async fn git_exec_routes_to_the_git_row_stub_with_all_features() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, "git-upload-pack '/owner/demo'") .await .expect("the exec request rides the wire"); expect_failure_then_close(&mut channel, "the git row serves its seam stub").await; session.unmount().await; } /// - *Contract stamp:* OQ-08 + the recorded wire shape (the /// unset-wrapper pipe row declines — the serving-path shape; /// `w3-tty-session`). The serving surface rides /// `tests/tty_mapping.rs`. #[tokio::test] async fn plain_exec_declines_wrapper_unconfigured_with_all_features() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_exec(&channel, "uptime") .await .expect("the exec request rides the wire"); expect_failure_then_close(&mut channel, "the unset-wrapper pipe row").await; session.unmount().await; } /// - *Contract stamp:* mapping.md §Channel-type rows + OQ-08 (the /// unset-seam pty/shell declines with the full graph on — the /// feature alone configures nothing; the serving surface rides /// `tests/tty_mapping.rs`). #[tokio::test] async fn pty_and_shell_decline_seam_unconfigured_with_all_features() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_pty(&channel) .await .expect("the pty request rides the wire"); expect_failure_then_close(&mut channel, "the unset-seam pty row").await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_shell(&channel) .await .expect("the shell request rides the wire"); expect_failure_then_close(&mut channel, "the unset-seam shell row").await; session.unmount().await; } /// - *Contract stamp:* the unset-declines posture (`w3-seam-config`, /// carried through `w3-tunnel-direct`: the route-1 bridge's /// pre-admit unset-handle class) — the direct rows decline on /// the **absent fabric handle** even with the full feature graph /// on (`ConnectFailed`, the cannot-bridge class); the served /// surface rides tests/tunnel_mapping.rs's fabric double. #[tokio::test] async fn tunnel_direct_rows_decline_seam_unconfigured_without_a_handle_with_all_features() { let session = authenticated().await; let err = open_direct_tcpip(&session.client, "internal.example", 5432) .await .expect_err("the full-featured direct-tcpip row declines on the absent handle"); open_failure( err, ChannelOpenFailure::ConnectFailed, "the direct-tcpip unset-handle decline", ); let err = open_direct_streamlocal(&session.client, "/run/alkssh.sock") .await .expect_err("the full-featured direct-streamlocal row declines on the absent handle"); open_failure( err, ChannelOpenFailure::ConnectFailed, "the direct-streamlocal unset-handle decline", ); session.unmount().await; } /// - *Contract stamp:* mapping.md §Request-level rows (the /// capability declines — sftp placeholder, git subsystem /// names, env — hold with everything on). #[tokio::test] async fn capability_declines_hold_with_all_features() { let session = authenticated().await; let mut channel = open_session_channel(&session.client) .await .expect("the session channel opens"); request_subsystem(&channel, "sftp") .await .expect("the subsystem request rides the wire"); expect_failure(&mut channel, "the sftp placeholder declines").await; request_subsystem(&channel, "git-upload-pack") .await .expect("the subsystem request rides the wire"); expect_failure(&mut channel, "the git subsystem declines").await; request_env(&channel, "GIT_DIR", "/srv/git") .await .expect("the env request rides the wire"); expect_failure(&mut channel, "the env request declines").await; expect_channel_alive(&mut channel).await; session.unmount().await; } }