From 30072d8e21a092682e748ba2bb74dd0838f95841 Mon Sep 17 00:00:00 2001 From: Gudge Date: Tue, 18 Aug 2026 20:07:22 -0700 Subject: [PATCH] Add 0.8 state-aware configuration adapters This PR adds exhaustive state-aware adapters for the mutable 0.8.0-alpha configuration contract. Details * Map every state-aware lifecycle and provision request into the current wire model. * Preserve validated experimental data and source text in a neutral pre-normalization value. * Destructure contract marker fields by their qualified type pattern rather than discarding them, so widening a marker fails to compile at the adapters that hardcode its wire value. * Add development-request facade coverage plus comprehensive expected-wire and current-wire equivalence tests. Tests * `cargo fmt --all -- --check` * `cargo check -p wxc_common` * `cargo clippy -p wxc_common --all-targets -- -D warnings` * `cargo test -p wxc_common` Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1ef43cc5-5752-426e-a36e-eb9aaccfb9ad --- .../config_contract_adapters/dev/common.rs | 107 +++ .../src/config_contract_adapters/dev/mod.rs | 239 +++++- .../config_contract_adapters/dev/one_shot.rs | 107 +-- .../dev/state_aware.rs | 445 +++++++++++ .../dev/state_aware_tests/common.rs | 104 +++ .../dev/state_aware_tests/deprovision.rs | 192 +++++ .../dev/state_aware_tests/exec.rs | 279 +++++++ .../dev/state_aware_tests/mod.rs | 9 + .../dev/state_aware_tests/provision.rs | 701 ++++++++++++++++++ .../dev/state_aware_tests/start.rs | 192 +++++ .../dev/state_aware_tests/stop.rs | 192 +++++ .../src/config_contract_adapters/mod.rs | 9 +- src/core/wxc_common/src/lib.rs | 3 + src/core/wxc_common/src/state_aware_wire.rs | 15 + 14 files changed, 2484 insertions(+), 110 deletions(-) create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/common.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/common.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/deprovision.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/exec.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/mod.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/provision.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/start.rs create mode 100644 src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/stop.rs create mode 100644 src/core/wxc_common/src/state_aware_wire.rs diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/common.rs b/src/core/wxc_common/src/config_contract_adapters/dev/common.rs new file mode 100644 index 000000000..e9fdd3cae --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/common.rs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::wire; +use mxc_config_contract::dev as contract; +use mxc_config_contract::ContractVersion; + +pub(super) fn convert_version(value: contract::Version) -> &'static str { + match value { + contract::Version::V0_8_0Alpha => ContractVersion::V0_8_0Alpha.as_str(), + } +} + +pub(super) fn convert_process(value: contract::Process) -> wire::Process { + let contract::Process { + command_line, + cwd, + env, + timeout, + } = value; + wire::Process { + command_line: Some(command_line.into_inner()), + cwd: cwd.into_option(), + env: env.into_option(), + timeout: timeout.into_option(), + } +} + +pub(super) fn convert_filesystem(value: contract::Filesystem) -> wire::Filesystem { + let contract::Filesystem { + readwrite_paths, + readonly_paths, + denied_paths, + } = value; + wire::Filesystem { + readwrite_paths: readwrite_paths.into_option(), + readonly_paths: readonly_paths.into_option(), + denied_paths: denied_paths.into_option(), + } +} + +fn convert_default_network_policy(value: contract::DefaultNetworkPolicy) -> wire::NetworkPolicy { + match value { + contract::DefaultNetworkPolicy::Allow => wire::NetworkPolicy::Allow, + contract::DefaultNetworkPolicy::Block => wire::NetworkPolicy::Block, + } +} + +fn convert_network_enforcement_mode( + value: contract::NetworkEnforcementMode, +) -> wire::NetworkEnforcement { + match value { + contract::NetworkEnforcementMode::Capabilities => wire::NetworkEnforcement::Capabilities, + contract::NetworkEnforcementMode::Firewall => wire::NetworkEnforcement::Firewall, + contract::NetworkEnforcementMode::Both => wire::NetworkEnforcement::Both, + } +} + +pub(super) fn convert_network(value: contract::Network) -> wire::Network { + let contract::Network { + default_policy, + enforcement_mode, + allow_local_network, + allowed_hosts, + blocked_hosts, + proxy, + } = value; + wire::Network { + default_policy: default_policy + .into_option() + .map(convert_default_network_policy), + enforcement_mode: enforcement_mode + .into_option() + .map(convert_network_enforcement_mode), + allow_local_network: allow_local_network.into_option(), + allowed_hosts: allowed_hosts.into_option(), + blocked_hosts: blocked_hosts.into_option(), + proxy: proxy.into_option().map(convert_proxy), + } +} + +fn convert_proxy(value: contract::NetworkProxy) -> wire::Proxy { + match value { + contract::NetworkProxy::Localhost(port) => wire::Proxy { + localhost: Some(port.get()), + builtin_test_server: None, + url: None, + }, + contract::NetworkProxy::BuiltinTestServer(contract::True) => wire::Proxy { + localhost: None, + builtin_test_server: Some(true), + url: None, + }, + contract::NetworkProxy::Url(url) => wire::Proxy { + localhost: None, + builtin_test_server: None, + url: Some(url), + }, + } +} + +pub(super) fn convert_telemetry(value: contract::Telemetry) -> wire::Telemetry { + let contract::Telemetry { enabled } = value; + wire::Telemetry { + enabled: enabled.into_option(), + } +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs b/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs index 83e878ea5..76b3831df 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs @@ -1,4 +1,241 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -pub(crate) mod one_shot; +mod common; +mod one_shot; +mod state_aware; + +use crate::state_aware_wire::StateAwareWireInput; +use crate::wire; + +use mxc_config_contract::dev as contract; + +pub(crate) enum AdaptedWireRequest { + OneShot(wire::MxcConfig), + StateAware(StateAwareWireInput), +} + +fn adapt_state_aware_wire( + config: wire::MxcConfig, + source_text: &str, +) -> Result { + state_aware::into_state_aware_wire_input(config, source_text) + .map(AdaptedWireRequest::StateAware) +} + +pub(crate) fn adapt_request( + request: contract::Request, + source_text: &str, +) -> Result { + match request { + contract::Request::OneShot(request) => { + Ok(AdaptedWireRequest::OneShot(one_shot::into_wire(*request))) + } + contract::Request::Provision(request) => { + adapt_state_aware_wire(state_aware::provision_into_wire(request), source_text) + } + contract::Request::Start(request) => { + adapt_state_aware_wire(state_aware::start_into_wire(request), source_text) + } + contract::Request::Exec(request) => { + adapt_state_aware_wire(state_aware::exec_into_wire(request), source_text) + } + contract::Request::Stop(request) => { + adapt_state_aware_wire(state_aware::stop_into_wire(request), source_text) + } + contract::Request::Deprovision(request) => { + adapt_state_aware_wire(state_aware::deprovision_into_wire(request), source_text) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn adapt_json(json: &str) -> AdaptedWireRequest { + let request = contract::parse_request(json).unwrap(); + adapt_request(request, json).unwrap() + } + + #[test] + fn one_shot_request_adapts_to_one_shot() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo hello" + } + }"#; + + let AdaptedWireRequest::OneShot(adapted) = adapt_json(json) else { + panic!("expected one-shot request"); + }; + + assert_eq!(adapted.version, Some("0.8.0-alpha".to_string())); + assert!(adapted.process.is_some()); + + let process = adapted.process.unwrap(); + assert_eq!(process.command_line, Some("echo hello".to_string())); + assert!(process.cwd.is_none()); + assert!(process.env.is_none()); + assert!(process.timeout.is_none()); + } + + #[test] + fn provision_request_adapts_to_state_aware() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox", + "experimental": { + "telemetry": { + "enabled": false + } + } + }"#; + + let AdaptedWireRequest::StateAware(adapted) = adapt_json(json) else { + panic!("expected state-aware request"); + }; + + assert_eq!(adapted.config.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(adapted.config.phase, Some(wire::Phase::Provision))); + assert_eq!( + adapted.experimental_raw, + Some(serde_json::json!({ + "telemetry": {"enabled": false} + })) + ); + assert_eq!(adapted.source_text.as_ref(), json); + } + + #[test] + fn start_request_adapts_to_state_aware() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id", + "experimental": { + "telemetry": { + "enabled": false + } + } + }"#; + + let AdaptedWireRequest::StateAware(adapted) = adapt_json(json) else { + panic!("expected state-aware request"); + }; + + assert_eq!(adapted.config.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(adapted.config.phase, Some(wire::Phase::Start))); + assert_eq!(adapted.config.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + adapted.experimental_raw, + Some(serde_json::json!({ + "telemetry": {"enabled": false} + })) + ); + assert_eq!(adapted.source_text.as_ref(), json); + } + + #[test] + fn exec_request_adapts_to_state_aware() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "sandbox-id", + "process": { + "commandLine": "echo hello" + }, + "experimental": { + "telemetry": { + "enabled": false + } + } + }"#; + + let AdaptedWireRequest::StateAware(adapted) = adapt_json(json) else { + panic!("expected state-aware request"); + }; + + assert_eq!(adapted.config.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(adapted.config.phase, Some(wire::Phase::Exec))); + assert_eq!(adapted.config.sandbox_id, Some("sandbox-id".to_string())); + assert!(adapted.config.process.is_some()); + + let process = adapted.config.process.unwrap(); + assert_eq!(process.command_line, Some("echo hello".to_string())); + assert!(process.cwd.is_none()); + assert!(process.env.is_none()); + assert!(process.timeout.is_none()); + + assert_eq!( + adapted.experimental_raw, + Some(serde_json::json!({ + "telemetry": {"enabled": false} + })) + ); + assert_eq!(adapted.source_text.as_ref(), json); + } + + #[test] + fn stop_request_adapts_to_state_aware() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "sandbox-id", + "experimental": { + "telemetry": { + "enabled": false + } + } + }"#; + + let AdaptedWireRequest::StateAware(adapted) = adapt_json(json) else { + panic!("expected state-aware request"); + }; + + assert_eq!(adapted.config.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(adapted.config.phase, Some(wire::Phase::Stop))); + assert_eq!(adapted.config.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + adapted.experimental_raw, + Some(serde_json::json!({ + "telemetry": {"enabled": false} + })) + ); + assert_eq!(adapted.source_text.as_ref(), json); + } + + #[test] + fn deprovision_request_adapts_to_state_aware() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "sandbox-id", + "experimental": { + "telemetry": { + "enabled": false + } + } + }"#; + + let AdaptedWireRequest::StateAware(adapted) = adapt_json(json) else { + panic!("expected state-aware request"); + }; + + assert_eq!(adapted.config.version, Some("0.8.0-alpha".to_string())); + assert!(matches!( + adapted.config.phase, + Some(wire::Phase::Deprovision) + )); + assert_eq!(adapted.config.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + adapted.experimental_raw, + Some(serde_json::json!({ + "telemetry": {"enabled": false} + })) + ); + assert_eq!(adapted.source_text.as_ref(), json); + } +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs index 5291720a3..bdc707e72 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs @@ -1,15 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::config_contract_adapters::dev::common::{ + convert_filesystem, convert_network, convert_process, convert_telemetry, convert_version, +}; use crate::wire; use mxc_config_contract::dev as contract; -use mxc_config_contract::ContractVersion; - -fn convert_version(value: contract::Version) -> &'static str { - match value { - contract::Version::V0_8_0Alpha => ContractVersion::V0_8_0Alpha.as_str(), - } -} fn convert_containment(value: contract::OneShotContainment) -> wire::Containment { match value { @@ -27,21 +23,6 @@ fn convert_containment(value: contract::OneShotContainment) -> wire::Containment } } -fn convert_process(value: contract::Process) -> wire::Process { - let contract::Process { - command_line, - cwd, - env, - timeout, - } = value; - wire::Process { - command_line: Some(command_line.into_inner()), - cwd: cwd.into_option(), - env: env.into_option(), - timeout: timeout.into_option(), - } -} - fn convert_lifecycle(value: contract::Lifecycle) -> wire::Lifecycle { let contract::Lifecycle { destroy_on_exit, @@ -53,19 +34,6 @@ fn convert_lifecycle(value: contract::Lifecycle) -> wire::Lifecycle { } } -fn convert_filesystem(value: contract::Filesystem) -> wire::Filesystem { - let contract::Filesystem { - readwrite_paths, - readonly_paths, - denied_paths, - } = value; - wire::Filesystem { - readwrite_paths: readwrite_paths.into_option(), - readonly_paths: readonly_paths.into_option(), - denied_paths: denied_paths.into_option(), - } -} - fn convert_fallback(value: contract::Fallback) -> wire::Fallback { let contract::Fallback { allow_dacl_mutation, @@ -75,66 +43,6 @@ fn convert_fallback(value: contract::Fallback) -> wire::Fallback { } } -fn convert_default_network_policy(value: contract::DefaultNetworkPolicy) -> wire::NetworkPolicy { - match value { - contract::DefaultNetworkPolicy::Allow => wire::NetworkPolicy::Allow, - contract::DefaultNetworkPolicy::Block => wire::NetworkPolicy::Block, - } -} - -fn convert_network_enforcement_mode( - value: contract::NetworkEnforcementMode, -) -> wire::NetworkEnforcement { - match value { - contract::NetworkEnforcementMode::Capabilities => wire::NetworkEnforcement::Capabilities, - contract::NetworkEnforcementMode::Firewall => wire::NetworkEnforcement::Firewall, - contract::NetworkEnforcementMode::Both => wire::NetworkEnforcement::Both, - } -} - -fn convert_network(value: contract::Network) -> wire::Network { - let contract::Network { - default_policy, - enforcement_mode, - allow_local_network, - allowed_hosts, - blocked_hosts, - proxy, - } = value; - wire::Network { - default_policy: default_policy - .into_option() - .map(convert_default_network_policy), - enforcement_mode: enforcement_mode - .into_option() - .map(convert_network_enforcement_mode), - allow_local_network: allow_local_network.into_option(), - allowed_hosts: allowed_hosts.into_option(), - blocked_hosts: blocked_hosts.into_option(), - proxy: proxy.into_option().map(convert_proxy), - } -} - -fn convert_proxy(value: contract::NetworkProxy) -> wire::Proxy { - match value { - contract::NetworkProxy::Localhost(port) => wire::Proxy { - localhost: Some(port.get()), - builtin_test_server: None, - url: None, - }, - contract::NetworkProxy::BuiltinTestServer(contract::True) => wire::Proxy { - localhost: None, - builtin_test_server: Some(true), - url: None, - }, - contract::NetworkProxy::Url(url) => wire::Proxy { - localhost: None, - builtin_test_server: None, - url: Some(url), - }, - } -} - fn convert_clipboard(value: contract::UiClipboard) -> wire::ClipboardPolicy { match value { contract::UiClipboard::None => wire::ClipboardPolicy::None, @@ -327,13 +235,6 @@ fn convert_wslc(value: contract::OneShotWslc) -> wire::Wslc { } } -fn convert_telemetry(value: contract::Telemetry) -> wire::Telemetry { - let contract::Telemetry { enabled } = value; - wire::Telemetry { - enabled: enabled.into_option(), - } -} - fn convert_experimental(value: contract::OneShotExperimental) -> wire::Experimental { let contract::OneShotExperimental { test, @@ -351,7 +252,7 @@ fn convert_experimental(value: contract::OneShotExperimental) -> wire::Experimen } } -pub(crate) fn into_wire(request: contract::OneShotRequest) -> wire::MxcConfig { +pub(super) fn into_wire(request: contract::OneShotRequest) -> wire::MxcConfig { let contract::OneShotRequest { schema, comment, diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs new file mode 100644 index 000000000..fa0d0b984 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware.rs @@ -0,0 +1,445 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::config_contract_adapters::dev::common::{ + convert_filesystem, convert_network, convert_process, convert_telemetry, convert_version, +}; +use crate::state_aware_wire::StateAwareWireInput; +use crate::wire; +use mxc_config_contract::dev as contract; + +#[derive(serde::Deserialize)] +struct ExperimentalProbe { + #[serde(default)] + experimental: Option, +} + +fn extract_experimental_value( + source_text: &str, +) -> Result, serde_json::Error> { + serde_json::from_str::(source_text).map(|probe| probe.experimental) +} + +fn convert_state_aware_isolation_session( + value: contract::StateAwareIsolationSession, +) -> wire::IsolationSession { + let contract::StateAwareIsolationSession { provision } = value; + wire::IsolationSession { + provision: provision + .into_option() + .map(convert_isolation_session_provision), + } +} + +fn convert_isolation_session_provision( + value: contract::IsolationSessionProvision, +) -> wire::IsolationSessionProvisionPhase { + let contract::IsolationSessionProvision { app_id } = value; + wire::IsolationSessionProvisionPhase { + app_id: app_id.into_option(), + } +} + +fn convert_isolation_session_provision_experimental( + value: contract::IsolationSessionProvisionExperimental, +) -> wire::Experimental { + let contract::IsolationSessionProvisionExperimental { + isolation_session, + telemetry, + } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: None, + isolation_session: isolation_session + .into_option() + .map(convert_state_aware_isolation_session), + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +fn convert_isolation_session_network(value: contract::IsolationSessionNetwork) -> wire::Network { + let contract::IsolationSessionNetwork { + allow_local_network: contract::True, + default_policy: contract::IsolationSessionNetworkDefaultPolicy, + } = value; + wire::Network { + allow_local_network: Some(true), + default_policy: Some(wire::NetworkPolicy::Allow), + allowed_hosts: None, + enforcement_mode: None, + blocked_hosts: None, + proxy: None, + } +} + +fn convert_windows_sandbox_provision_experimental( + value: contract::WindowsSandboxExperimental, +) -> wire::Experimental { + let contract::WindowsSandboxExperimental { telemetry } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: None, + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +fn convert_wslc_provision(value: contract::WslcProvision) -> wire::WslcProvisionPhase { + let contract::WslcProvision { + image, + image_tar_path, + } = value; + wire::WslcProvisionPhase { + image: image.into_option(), + image_tar_path: image_tar_path.into_option(), + } +} + +fn convert_state_aware_wslc(value: contract::StateAwareWslc) -> wire::Wslc { + let contract::StateAwareWslc { provision } = value; + wire::Wslc { + cpu_count: None, + gpu: None, + image: None, + image_tar_path: None, + memory_mb: None, + port_mappings: None, + storage_path: None, + target_os: None, + provision: provision.into_option().map(convert_wslc_provision), + } +} + +fn convert_wslc_provision_experimental( + value: contract::WslcProvisionExperimental, +) -> wire::Experimental { + let contract::WslcProvisionExperimental { wslc, telemetry } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: wslc.into_option().map(convert_state_aware_wslc), + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +fn convert_start_experimental(value: contract::StartExperimental) -> wire::Experimental { + let contract::StartExperimental { telemetry } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: None, + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +fn convert_exec_experimental(value: contract::ExecExperimental) -> wire::Experimental { + let contract::ExecExperimental { telemetry } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: None, + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +fn convert_stop_experimental(value: contract::StopExperimental) -> wire::Experimental { + let contract::StopExperimental { telemetry } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: None, + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +fn convert_deprovision_experimental( + value: contract::DeprovisionExperimental, +) -> wire::Experimental { + let contract::DeprovisionExperimental { telemetry } = value; + wire::Experimental { + test: None, + windows_sandbox: None, + wslc: None, + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +pub(super) fn provision_into_wire(request: contract::ProvisionRequest) -> wire::MxcConfig { + match request { + contract::ProvisionRequest::IsolationSession(request) => { + isolation_session_provision_into_wire(request) + } + contract::ProvisionRequest::WindowsSandbox(request) => { + windows_sandbox_provision_into_wire(request) + } + contract::ProvisionRequest::Wslc(request) => wslc_provision_into_wire(request), + } +} + +fn isolation_session_provision_into_wire( + request: contract::IsolationSessionProvisionRequest, +) -> wire::MxcConfig { + let contract::IsolationSessionProvisionRequest { + schema, + comment, + version, + phase: contract::ProvisionPhase, + containment: contract::IsolationSessionContainment, + network, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Provision), + experimental: experimental + .into_option() + .map(convert_isolation_session_provision_experimental), + containment: Some(wire::Containment::IsolationSession), + container_id: None, + correlation_vector: None, + sandbox_id: None, + process: None, + filesystem: None, + fallback: None, + network: Some(convert_isolation_session_network(network)), + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +fn windows_sandbox_provision_into_wire( + request: contract::WindowsSandboxProvisionRequest, +) -> wire::MxcConfig { + let contract::WindowsSandboxProvisionRequest { + schema, + comment, + version, + phase: contract::ProvisionPhase, + containment: contract::WindowsSandboxContainment, + filesystem, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Provision), + experimental: experimental + .into_option() + .map(convert_windows_sandbox_provision_experimental), + containment: Some(wire::Containment::WindowsSandbox), + container_id: None, + correlation_vector: None, + sandbox_id: None, + process: None, + filesystem: filesystem.into_option().map(convert_filesystem), + fallback: None, + network: None, + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +fn wslc_provision_into_wire(request: contract::WslcProvisionRequest) -> wire::MxcConfig { + let contract::WslcProvisionRequest { + schema, + comment, + version, + phase: contract::ProvisionPhase, + containment: contract::WslcContainment, + filesystem, + network, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Provision), + experimental: experimental + .into_option() + .map(convert_wslc_provision_experimental), + containment: Some(wire::Containment::Wslc), + container_id: None, + correlation_vector: None, + sandbox_id: None, + process: None, + filesystem: filesystem.into_option().map(convert_filesystem), + fallback: None, + network: network.into_option().map(convert_network), + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +pub(super) fn start_into_wire(request: contract::StartRequest) -> wire::MxcConfig { + let contract::StartRequest { + schema, + comment, + version, + phase: contract::StartPhase, + sandbox_id, + correlation_vector, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Start), + sandbox_id: Some(sandbox_id), + correlation_vector: correlation_vector.into_option(), + experimental: experimental.into_option().map(convert_start_experimental), + containment: None, + container_id: None, + process: None, + filesystem: None, + fallback: None, + network: None, + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +pub(super) fn exec_into_wire(request: contract::ExecRequest) -> wire::MxcConfig { + let contract::ExecRequest { + schema, + comment, + version, + phase: contract::ExecPhase, + sandbox_id, + process, + correlation_vector, + network, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Exec), + sandbox_id: Some(sandbox_id), + correlation_vector: correlation_vector.into_option(), + experimental: experimental.into_option().map(convert_exec_experimental), + containment: None, + container_id: None, + process: Some(convert_process(process)), + filesystem: None, + fallback: None, + network: network.into_option().map(convert_network), + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +pub(super) fn stop_into_wire(request: contract::StopRequest) -> wire::MxcConfig { + let contract::StopRequest { + schema, + comment, + version, + phase: contract::StopPhase, + sandbox_id, + correlation_vector, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Stop), + sandbox_id: Some(sandbox_id), + correlation_vector: correlation_vector.into_option(), + experimental: experimental.into_option().map(convert_stop_experimental), + containment: None, + container_id: None, + process: None, + filesystem: None, + fallback: None, + network: None, + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +pub(super) fn deprovision_into_wire(request: contract::DeprovisionRequest) -> wire::MxcConfig { + let contract::DeprovisionRequest { + schema, + comment, + version, + phase: contract::DeprovisionPhase, + sandbox_id, + correlation_vector, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: Some(wire::Phase::Deprovision), + sandbox_id: Some(sandbox_id), + correlation_vector: correlation_vector.into_option(), + experimental: experimental + .into_option() + .map(convert_deprovision_experimental), + containment: None, + container_id: None, + process: None, + filesystem: None, + fallback: None, + network: None, + lifecycle: None, + lxc: None, + process_container: None, + seatbelt: None, + ui: None, + } +} + +pub(super) fn into_state_aware_wire_input( + config: wire::MxcConfig, + source_text: &str, +) -> Result { + Ok(StateAwareWireInput { + config, + experimental_raw: extract_experimental_value(source_text)?, + source_text: source_text.into(), + }) +} + +#[cfg(test)] +#[path = "state_aware_tests/mod.rs"] +mod tests; diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/common.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/common.rs new file mode 100644 index 000000000..bd77c7e9d --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/common.rs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::{ + contract, extract_experimental_value, into_state_aware_wire_input, start_into_wire, wire, +}; +use crate::state_aware_wire::StateAwareWireInput; + +#[test] +fn extract_experimental_value_returns_none_when_absent() { + let source = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id" + }"#; + + assert_eq!(extract_experimental_value(source).unwrap(), None); +} + +#[test] +fn extract_experimental_value_preserves_empty_object() { + let source = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id", + "experimental": {} + }"#; + + assert_eq!( + extract_experimental_value(source).unwrap(), + Some(serde_json::json!({})) + ); +} + +#[test] +fn extract_experimental_value_preserves_backend_payload_and_telemetry() { + let source = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "experimental": { + "isolation_session": { + "provision": { + "appId": "someAppId" + } + }, + "telemetry": { + "enabled": false + } + } + }"#; + + assert_eq!( + extract_experimental_value(source).unwrap(), + Some(serde_json::json!({ + "isolation_session": { + "provision": { + "appId": "someAppId" + } + }, + "telemetry": { + "enabled": false + } + })) + ); +} + +#[test] +fn into_state_aware_wire_input_packages_config_raw_value_and_source_text() { + let source = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id", + "experimental": { + "telemetry": { + "enabled": false + } + } + }"#; + let request: contract::StartRequest = serde_json::from_str(source).unwrap(); + let config = start_into_wire(request); + + let StateAwareWireInput { + config, + experimental_raw, + source_text, + } = into_state_aware_wire_input(config, source).unwrap(); + + assert!(matches!(config.phase, Some(wire::Phase::Start))); + assert_eq!(config.sandbox_id.as_deref(), Some("sandbox-id")); + assert_eq!( + experimental_raw, + Some(serde_json::json!({ + "telemetry": { + "enabled": false + } + })) + ); + assert_eq!(source_text.as_ref(), source); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/deprovision.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/deprovision.rs new file mode 100644 index 000000000..f424768b9 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/deprovision.rs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::{contract, deprovision_into_wire, wire}; + +const MINIMAL_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "sandbox-id" +}"#; + +const ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/deprovision.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "sandbox-id", + "correlationVector": "correlation-vector", + "experimental": { + "telemetry": { + "enabled": false + } + } +}"#; + +fn request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "sandbox-id", + {fields} + }}"# + ) +} + +pub(super) fn adapt(json: &str) -> wire::MxcConfig { + let request: contract::DeprovisionRequest = serde_json::from_str(json).unwrap(); + deprovision_into_wire(request) +} + +#[test] +fn minimal_request_maps_expected_wire_fields() { + let json = MINIMAL_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Deprovision))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn request_with_all_fields_maps_expected_wire_fields() { + let json = ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/deprovision.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Deprovision))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + wire.correlation_vector, + Some("correlation-vector".to_string()) + ); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); +} + +#[test] +fn empty_experimental_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&request_with_fields(r#""experimental": {}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + + let wire = adapt(&request_with_fields(r#""experimental": {"telemetry": {}}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); +} + +#[test] +fn empty_identifier_strings_map_expected_wire_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "", + "correlationVector": "" + }"#; + + let wire = adapt(json); + assert_eq!(wire.sandbox_id.as_deref(), Some("")); + assert_eq!(wire.correlation_vector.as_deref(), Some("")); +} + +#[test] +fn null_comment_maps_expected_wire_field() { + let wire = adapt(&request_with_fields(r#""_comment": null"#)); + assert_eq!(wire.comment.as_ref(), Some(&serde_json::Value::Null)); +} + +// Deserialization match tests +pub(super) fn assert_matches_current_wire_deserialization(json: &str) { + let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap(); + let adapted = adapt(json); + + assert_eq!( + serde_json::to_value(adapted).unwrap(), + serde_json::to_value(current).unwrap() + ); +} + +#[test] +fn minimal_request_matches_current_wire_deserialization() { + let json = MINIMAL_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn request_with_all_fields_matches_current_wire_deserialization() { + let json = ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn empty_experimental_sections_match_current_wire_deserialization() { + for fields in [ + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_matches_current_wire_deserialization(&request_with_fields(fields)); + } +} + +#[test] +fn empty_identifier_strings_match_current_wire_deserialization() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "", + "correlationVector": "" + }"#; + + assert_matches_current_wire_deserialization(json); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/exec.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/exec.rs new file mode 100644 index 000000000..271505d91 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/exec.rs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::{contract, exec_into_wire, wire}; + +const MINIMAL_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "sandbox-id", + "process": { + "commandLine": "echo hello" + } +}"#; + +const ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/exec.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "sandbox-id", + "correlationVector": "correlation-vector", + "process": { + "commandLine": "echo hello", + "cwd": "/work", + "env": ["FIRST=one", "SECOND=two"], + "timeout": 60 + }, + "network": { + "defaultPolicy": "allow", + "enforcementMode": "both", + "allowLocalNetwork": false, + "allowedHosts": ["example.com"], + "blockedHosts": ["blocked.example.com"], + "proxy": { + "url": "http://127.0.0.1:8080" + } + }, + "experimental": { + "telemetry": { + "enabled": false + } + } +}"#; + +fn request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "sandbox-id", + "process": {{"commandLine": "echo hello"}}, + {fields} + }}"# + ) +} + +pub(super) fn adapt(json: &str) -> wire::MxcConfig { + let request: contract::ExecRequest = serde_json::from_str(json).unwrap(); + exec_into_wire(request) +} + +#[test] +fn minimal_request_maps_expected_wire_fields() { + let json = MINIMAL_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Exec))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + + let process = wire.process.expect("process should be populated"); + assert_eq!(process.command_line.as_deref(), Some("echo hello")); + assert!(process.cwd.is_none()); + assert!(process.env.is_none()); + assert!(process.timeout.is_none()); + + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn request_with_all_fields_maps_expected_wire_fields() { + let json = ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/exec.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Exec))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + wire.correlation_vector, + Some("correlation-vector".to_string()) + ); + + let process = wire.process.expect("process should be populated"); + assert_eq!(process.command_line.as_deref(), Some("echo hello")); + assert_eq!(process.cwd.as_deref(), Some("/work")); + assert_eq!( + process.env.as_deref(), + Some(["FIRST=one".to_string(), "SECOND=two".to_string()].as_slice()) + ); + assert_eq!(process.timeout, Some(60)); + + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + + let network = wire.network.expect("network should be populated"); + assert!(matches!( + network.default_policy, + Some(wire::NetworkPolicy::Allow) + )); + assert!(matches!( + network.enforcement_mode, + Some(wire::NetworkEnforcement::Both) + )); + assert_eq!(network.allow_local_network, Some(false)); + assert_eq!( + network.allowed_hosts.as_deref(), + Some(["example.com".to_string()].as_slice()) + ); + assert_eq!( + network.blocked_hosts.as_deref(), + Some(["blocked.example.com".to_string()].as_slice()) + ); + let proxy = network.proxy.expect("proxy should be populated"); + assert!(proxy.localhost.is_none()); + assert!(proxy.builtin_test_server.is_none()); + assert_eq!(proxy.url.as_deref(), Some("http://127.0.0.1:8080")); + + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); +} + +#[test] +fn empty_experimental_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&request_with_fields(r#""experimental": {}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + + let wire = adapt(&request_with_fields(r#""experimental": {"telemetry": {}}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); +} + +#[test] +fn empty_network_section_maps_to_present_empty_wire_section() { + let wire = adapt(&request_with_fields(r#""network": {}"#)); + let network = wire.network.expect("network should be populated"); + assert!(network.default_policy.is_none()); + assert!(network.enforcement_mode.is_none()); + assert!(network.allow_local_network.is_none()); + assert!(network.allowed_hosts.is_none()); + assert!(network.blocked_hosts.is_none()); + assert!(network.proxy.is_none()); +} + +#[test] +fn empty_identifier_strings_map_expected_wire_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "", + "correlationVector": "", + "process": { + "commandLine": "echo hello", + "cwd": "" + } + }"#; + + let wire = adapt(json); + assert_eq!(wire.sandbox_id.as_deref(), Some("")); + assert_eq!(wire.correlation_vector.as_deref(), Some("")); + assert_eq!( + wire.process + .as_ref() + .and_then(|process| process.cwd.as_deref()), + Some("") + ); +} + +#[test] +fn null_comment_maps_expected_wire_field() { + let wire = adapt(&request_with_fields(r#""_comment": null"#)); + assert_eq!(wire.comment.as_ref(), Some(&serde_json::Value::Null)); +} + +// Deserialization match tests +pub(super) fn assert_matches_current_wire_deserialization(json: &str) { + let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap(); + let adapted = adapt(json); + + assert_eq!( + serde_json::to_value(adapted).unwrap(), + serde_json::to_value(current).unwrap() + ); +} + +#[test] +fn minimal_request_matches_current_wire_deserialization() { + let json = MINIMAL_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn request_with_all_fields_matches_current_wire_deserialization() { + let json = ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn empty_experimental_sections_match_current_wire_deserialization() { + for fields in [ + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_matches_current_wire_deserialization(&request_with_fields(fields)); + } +} + +#[test] +fn empty_network_section_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(&request_with_fields(r#""network": {}"#)); +} + +#[test] +fn empty_identifier_strings_match_current_wire_deserialization() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "", + "correlationVector": "", + "process": { + "commandLine": "echo hello", + "cwd": "" + } + }"#; + + assert_matches_current_wire_deserialization(json); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/mod.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/mod.rs new file mode 100644 index 000000000..bada1f8d3 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod common; +mod deprovision; +mod exec; +mod provision; +mod start; +mod stop; diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/provision.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/provision.rs new file mode 100644 index 000000000..8a6b51ffb --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/provision.rs @@ -0,0 +1,701 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::{contract, provision_into_wire, wire}; + +const MINIMAL_ISOLATION_SESSION_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "allowLocalNetwork": true, + "defaultPolicy": "allow" + } +}"#; + +const ISOLATION_SESSION_ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/provision.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "allowLocalNetwork": true, + "defaultPolicy": "allow" + }, + "experimental": { + "isolation_session": { + "provision": { + "appId": "someAppId" + } + }, + "telemetry": { + "enabled": false + } + } +}"#; + +const MINIMAL_WINDOWS_SANDBOX_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox" +}"#; + +const WINDOWS_SANDBOX_ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/provision.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox", + "filesystem": { + "readonlyPaths": ["C:\\Windows\\System32"], + "readwritePaths": ["C:\\Users\\User\\Documents"], + "deniedPaths": ["C:\\Users\\User\\Music"] + }, + "experimental": { + "telemetry": { + "enabled": false + } + } +}"#; + +const MINIMAL_WSLC_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc" +}"#; + +const WSLC_ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/provision.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc", + "filesystem": { + "readonlyPaths": ["/usr/bin"], + "readwritePaths": ["/home/user/documents"], + "deniedPaths": ["/home/user/music"] + }, + "network": { + "allowLocalNetwork": false, + "defaultPolicy": "block", + "enforcementMode": "firewall", + "allowedHosts": ["example.com"], + "blockedHosts": ["blocked.example.com"], + "proxy": { + "url": "http://example.com/proxy" + } + }, + "experimental": { + "wslc": { + "provision": { + "image": "someImage", + "imageTarPath": "someImageTarPath" + } + }, + "telemetry": { + "enabled": false + } + } +}"#; + +pub(super) fn adapt(json: &str) -> wire::MxcConfig { + let request = contract::parse_request(json).unwrap(); + + let contract::Request::Provision(request) = request else { + panic!("expected a ProvisionRequest"); + }; + provision_into_wire(request) +} + +fn isolation_session_request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": {{ + "allowLocalNetwork": true, + "defaultPolicy": "allow" + }}, + {fields} + }}"# + ) +} + +fn windows_sandbox_request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox", + {fields} + }}"# + ) +} + +fn wslc_request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc", + {fields} + }}"# + ) +} + +#[test] +fn minimal_isolation_session_request_maps_expected_wire_fields() { + let json = MINIMAL_ISOLATION_SESSION_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Provision))); + assert!(matches!( + wire.containment, + Some(wire::Containment::IsolationSession) + )); + + let network = wire.network.expect("network should be present"); + assert_eq!(network.allow_local_network, Some(true)); + assert!(matches!( + network.default_policy, + Some(wire::NetworkPolicy::Allow) + )); + assert!(network.enforcement_mode.is_none()); + assert!(network.allowed_hosts.is_none()); + assert!(network.blocked_hosts.is_none()); + + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn isolation_session_request_maps_expected_wire_fields() { + let json = ISOLATION_SESSION_ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/provision.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Provision))); + assert!(matches!( + wire.containment, + Some(wire::Containment::IsolationSession) + )); + let network = wire.network.expect("network should be present"); + assert_eq!(network.allow_local_network, Some(true)); + assert!(matches!( + network.default_policy, + Some(wire::NetworkPolicy::Allow) + )); + assert!(network.enforcement_mode.is_none()); + assert!(network.allowed_hosts.is_none()); + assert!(network.blocked_hosts.is_none()); + + let experimental = wire.experimental.expect("experimental should be present"); + let isolation_session = experimental + .isolation_session + .expect("isolation_session should be present"); + let provision = isolation_session + .provision + .expect("provision should be present"); + assert_eq!(provision.app_id.as_deref(), Some("someAppId")); + + let telemetry = experimental.telemetry.expect("telemetry should be present"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.seatbelt.is_none()); + + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); +} + +#[test] +fn minimal_windows_sandbox_request_maps_expected_wire_fields() { + let json = MINIMAL_WINDOWS_SANDBOX_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Provision))); + assert!(matches!( + wire.containment, + Some(wire::Containment::WindowsSandbox) + )); + + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.network.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn windows_sandbox_request_maps_expected_wire_fields() { + let json = WINDOWS_SANDBOX_ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/provision.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Provision))); + assert!(matches!( + wire.containment, + Some(wire::Containment::WindowsSandbox) + )); + + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert_eq!( + filesystem.readonly_paths, + Some(vec!["C:\\Windows\\System32".to_string()]) + ); + assert_eq!( + filesystem.readwrite_paths, + Some(vec!["C:\\Users\\User\\Documents".to_string()]) + ); + assert_eq!( + filesystem.denied_paths, + Some(vec!["C:\\Users\\User\\Music".to_string()]) + ); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.seatbelt.is_none()); + + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.network.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); +} + +#[test] +fn minimal_wslc_request_maps_expected_wire_fields() { + let json = MINIMAL_WSLC_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Provision))); + assert!(matches!(wire.containment, Some(wire::Containment::Wslc))); + + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.network.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn wslc_request_maps_expected_wire_fields() { + let json = WSLC_ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/provision.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Provision))); + assert!(matches!(wire.containment, Some(wire::Containment::Wslc))); + + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert_eq!( + filesystem.readonly_paths, + Some(vec!["/usr/bin".to_string()]) + ); + assert_eq!( + filesystem.readwrite_paths, + Some(vec!["/home/user/documents".to_string()]) + ); + assert_eq!( + filesystem.denied_paths, + Some(vec!["/home/user/music".to_string()]) + ); + + let network = wire.network.expect("network should be populated"); + assert_eq!(network.allow_local_network, Some(false)); + assert!(matches!( + network.default_policy, + Some(wire::NetworkPolicy::Block) + )); + assert!(matches!( + network.enforcement_mode, + Some(wire::NetworkEnforcement::Firewall) + )); + assert_eq!(network.allowed_hosts, Some(vec!["example.com".to_string()])); + assert_eq!( + network.blocked_hosts, + Some(vec!["blocked.example.com".to_string()]) + ); + assert!(network.proxy.is_some()); + assert_eq!( + network.proxy.as_ref().unwrap().url, + Some("http://example.com/proxy".to_string()) + ); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let wslc = experimental.wslc.expect("wslc should be populated"); + let provision = wslc.provision.expect("provision should be populated"); + assert_eq!(provision.image.as_deref(), Some("someImage")); + assert_eq!( + provision.image_tar_path.as_deref(), + Some("someImageTarPath") + ); + assert!(wslc.target_os.is_none()); + assert!(wslc.image.is_none()); + assert!(wslc.image_tar_path.is_none()); + assert!(wslc.cpu_count.is_none()); + assert!(wslc.memory_mb.is_none()); + assert!(wslc.gpu.is_none()); + assert!(wslc.storage_path.is_none()); + assert!(wslc.port_mappings.is_none()); + + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.seatbelt.is_none()); + + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); +} + +#[test] +fn empty_isolation_session_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&isolation_session_request_with_fields( + r#""experimental": {}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + assert!(experimental.isolation_session.is_none()); + + let wire = adapt(&isolation_session_request_with_fields( + r#""experimental": {"telemetry": {}}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); + + let wire = adapt(&isolation_session_request_with_fields( + r#""experimental": {"isolation_session": {}}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + let isolation_session = experimental + .isolation_session + .expect("isolation_session should be populated"); + assert!(isolation_session.provision.is_none()); + + let wire = adapt(&isolation_session_request_with_fields( + r#""experimental": {"isolation_session": {"provision": {}}}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + let isolation_session = experimental + .isolation_session + .expect("isolation_session should be populated"); + let provision = isolation_session + .provision + .expect("provision should be populated"); + assert!(provision.app_id.is_none()); +} + +#[test] +fn empty_windows_sandbox_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&windows_sandbox_request_with_fields(r#""filesystem": {}"#)); + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert!(filesystem.readwrite_paths.is_none()); + assert!(filesystem.readonly_paths.is_none()); + assert!(filesystem.denied_paths.is_none()); + + let wire = adapt(&windows_sandbox_request_with_fields( + r#""experimental": {}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + + let wire = adapt(&windows_sandbox_request_with_fields( + r#""experimental": {"telemetry": {}}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); +} + +#[test] +fn empty_wslc_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&wslc_request_with_fields(r#""filesystem": {}"#)); + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert!(filesystem.readwrite_paths.is_none()); + assert!(filesystem.readonly_paths.is_none()); + assert!(filesystem.denied_paths.is_none()); + + let wire = adapt(&wslc_request_with_fields(r#""network": {}"#)); + let network = wire.network.expect("network should be populated"); + assert!(network.default_policy.is_none()); + assert!(network.enforcement_mode.is_none()); + assert!(network.allow_local_network.is_none()); + assert!(network.allowed_hosts.is_none()); + assert!(network.blocked_hosts.is_none()); + assert!(network.proxy.is_none()); + + let wire = adapt(&wslc_request_with_fields(r#""experimental": {}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + assert!(experimental.wslc.is_none()); + + let wire = adapt(&wslc_request_with_fields( + r#""experimental": {"telemetry": {}}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); + + let wire = adapt(&wslc_request_with_fields(r#""experimental": {"wslc": {}}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + let wslc = experimental.wslc.expect("wslc should be populated"); + assert!(wslc.provision.is_none()); + + let wire = adapt(&wslc_request_with_fields( + r#""experimental": {"wslc": {"provision": {}}}"#, + )); + let experimental = wire.experimental.expect("experimental should be populated"); + let wslc = experimental.wslc.expect("wslc should be populated"); + let provision = wslc.provision.expect("provision should be populated"); + assert!(provision.image.is_none()); + assert!(provision.image_tar_path.is_none()); +} + +#[test] +fn empty_isolation_session_app_id_maps_expected_wire_field() { + let wire = adapt(&isolation_session_request_with_fields( + r#""experimental": {"isolation_session": {"provision": {"appId": ""}}}"#, + )); + let app_id = wire + .experimental + .and_then(|experimental| experimental.isolation_session) + .and_then(|isolation_session| isolation_session.provision) + .and_then(|provision| provision.app_id); + assert_eq!(app_id.as_deref(), Some("")); +} + +#[test] +fn empty_wslc_provision_strings_map_expected_wire_fields() { + let wire = adapt(&wslc_request_with_fields( + r#""experimental": {"wslc": {"provision": {"image": "", "imageTarPath": ""}}}"#, + )); + let provision = wire + .experimental + .and_then(|experimental| experimental.wslc) + .and_then(|wslc| wslc.provision) + .expect("provision should be populated"); + assert_eq!(provision.image.as_deref(), Some("")); + assert_eq!(provision.image_tar_path.as_deref(), Some("")); +} + +#[test] +fn null_provision_comments_map_expected_wire_fields() { + for json in [ + isolation_session_request_with_fields(r#""_comment": null"#), + windows_sandbox_request_with_fields(r#""_comment": null"#), + wslc_request_with_fields(r#""_comment": null"#), + ] { + let wire = adapt(&json); + assert_eq!(wire.comment.as_ref(), Some(&serde_json::Value::Null)); + } +} + +// Deserialization match tests +pub(super) fn assert_matches_current_wire_deserialization(json: &str) { + let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap(); + let adapted = adapt(json); + + assert_eq!( + serde_json::to_value(adapted).unwrap(), + serde_json::to_value(current).unwrap() + ); +} + +#[test] +fn minimal_isolation_session_request_matches_current_wire_deserialization() { + let json = MINIMAL_ISOLATION_SESSION_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn isolation_session_request_matches_current_wire_deserialization() { + let json = ISOLATION_SESSION_ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn minimal_windows_sandbox_request_matches_current_wire_deserialization() { + let json = MINIMAL_WINDOWS_SANDBOX_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn windows_sandbox_request_matches_current_wire_deserialization() { + let json = WINDOWS_SANDBOX_ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn minimal_wslc_request_matches_current_wire_deserialization() { + let json = MINIMAL_WSLC_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn wslc_request_matches_current_wire_deserialization() { + let json = WSLC_ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn empty_isolation_session_sections_match_current_wire_deserialization() { + for fields in [ + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + r#""experimental": {"isolation_session": {}}"#, + r#""experimental": {"isolation_session": {"provision": {}}}"#, + ] { + assert_matches_current_wire_deserialization(&isolation_session_request_with_fields(fields)); + } +} + +#[test] +fn empty_windows_sandbox_sections_match_current_wire_deserialization() { + for fields in [ + r#""filesystem": {}"#, + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_matches_current_wire_deserialization(&windows_sandbox_request_with_fields(fields)); + } +} + +#[test] +fn empty_wslc_sections_match_current_wire_deserialization() { + for fields in [ + r#""filesystem": {}"#, + r#""network": {}"#, + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + r#""experimental": {"wslc": {}}"#, + r#""experimental": {"wslc": {"provision": {}}}"#, + ] { + assert_matches_current_wire_deserialization(&wslc_request_with_fields(fields)); + } +} + +#[test] +fn empty_backend_strings_match_current_wire_deserialization() { + let isolation_session = isolation_session_request_with_fields( + r#""experimental": {"isolation_session": {"provision": {"appId": ""}}}"#, + ); + assert_matches_current_wire_deserialization(&isolation_session); + + let wslc = wslc_request_with_fields( + r#""experimental": {"wslc": {"provision": {"image": "", "imageTarPath": ""}}}"#, + ); + assert_matches_current_wire_deserialization(&wslc); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/start.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/start.rs new file mode 100644 index 000000000..737ee60dc --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/start.rs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::{contract, start_into_wire, wire}; + +const MINIMAL_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id" +}"#; + +const ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/start.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id", + "correlationVector": "correlation-vector", + "experimental": { + "telemetry": { + "enabled": false + } + } +}"#; + +fn request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "sandbox-id", + {fields} + }}"# + ) +} + +pub(super) fn adapt(json: &str) -> wire::MxcConfig { + let request: contract::StartRequest = serde_json::from_str(json).unwrap(); + start_into_wire(request) +} + +#[test] +fn minimal_request_maps_expected_wire_fields() { + let json = MINIMAL_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Start))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn request_with_all_fields_maps_expected_wire_fields() { + let json = ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/start.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Start))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + wire.correlation_vector, + Some("correlation-vector".to_string()) + ); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); +} + +#[test] +fn empty_experimental_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&request_with_fields(r#""experimental": {}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + + let wire = adapt(&request_with_fields(r#""experimental": {"telemetry": {}}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); +} + +#[test] +fn empty_identifier_strings_map_expected_wire_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "", + "correlationVector": "" + }"#; + + let wire = adapt(json); + assert_eq!(wire.sandbox_id.as_deref(), Some("")); + assert_eq!(wire.correlation_vector.as_deref(), Some("")); +} + +#[test] +fn null_comment_maps_expected_wire_field() { + let wire = adapt(&request_with_fields(r#""_comment": null"#)); + assert_eq!(wire.comment.as_ref(), Some(&serde_json::Value::Null)); +} + +// Deserialization match tests +pub(super) fn assert_matches_current_wire_deserialization(json: &str) { + let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap(); + let adapted = adapt(json); + + assert_eq!( + serde_json::to_value(adapted).unwrap(), + serde_json::to_value(current).unwrap() + ); +} + +#[test] +fn minimal_request_matches_current_wire_deserialization() { + let json = MINIMAL_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn request_with_all_fields_matches_current_wire_deserialization() { + let json = ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn empty_experimental_sections_match_current_wire_deserialization() { + for fields in [ + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_matches_current_wire_deserialization(&request_with_fields(fields)); + } +} + +#[test] +fn empty_identifier_strings_match_current_wire_deserialization() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "", + "correlationVector": "" + }"#; + + assert_matches_current_wire_deserialization(json); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/stop.rs b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/stop.rs new file mode 100644 index 000000000..d3e85790e --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/state_aware_tests/stop.rs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::{contract, stop_into_wire, wire}; + +const MINIMAL_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "sandbox-id" +}"#; + +const ALL_FIELDS_REQUEST_JSON: &str = r#"{ + "$schema": "https://example.com/stop.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "sandbox-id", + "correlationVector": "correlation-vector", + "experimental": { + "telemetry": { + "enabled": false + } + } +}"#; + +fn request_with_fields(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "sandbox-id", + {fields} + }}"# + ) +} + +pub(super) fn adapt(json: &str) -> wire::MxcConfig { + let request: contract::StopRequest = serde_json::from_str(json).unwrap(); + stop_into_wire(request) +} + +#[test] +fn minimal_request_maps_expected_wire_fields() { + let json = MINIMAL_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Stop))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn request_with_all_fields_maps_expected_wire_fields() { + let json = ALL_FIELDS_REQUEST_JSON; + + let wire = adapt(json); + + assert_eq!( + wire.schema, + Some("https://example.com/stop.schema.json".to_string()) + ); + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(matches!(wire.phase, Some(wire::Phase::Stop))); + assert_eq!(wire.sandbox_id, Some("sandbox-id".to_string())); + assert_eq!( + wire.correlation_vector, + Some("correlation-vector".to_string()) + ); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + assert!(wire.process.is_none()); + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.test.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); +} + +#[test] +fn empty_experimental_sections_map_to_present_empty_wire_sections() { + let wire = adapt(&request_with_fields(r#""experimental": {}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + assert!(experimental.telemetry.is_none()); + + let wire = adapt(&request_with_fields(r#""experimental": {"telemetry": {}}"#)); + let experimental = wire.experimental.expect("experimental should be populated"); + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert!(telemetry.enabled.is_none()); +} + +#[test] +fn empty_identifier_strings_map_expected_wire_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "", + "correlationVector": "" + }"#; + + let wire = adapt(json); + assert_eq!(wire.sandbox_id.as_deref(), Some("")); + assert_eq!(wire.correlation_vector.as_deref(), Some("")); +} + +#[test] +fn null_comment_maps_expected_wire_field() { + let wire = adapt(&request_with_fields(r#""_comment": null"#)); + assert_eq!(wire.comment.as_ref(), Some(&serde_json::Value::Null)); +} + +// Deserialization match tests +pub(super) fn assert_matches_current_wire_deserialization(json: &str) { + let current: wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap(); + let adapted = adapt(json); + + assert_eq!( + serde_json::to_value(adapted).unwrap(), + serde_json::to_value(current).unwrap() + ); +} + +#[test] +fn minimal_request_matches_current_wire_deserialization() { + let json = MINIMAL_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn request_with_all_fields_matches_current_wire_deserialization() { + let json = ALL_FIELDS_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn empty_experimental_sections_match_current_wire_deserialization() { + for fields in [ + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_matches_current_wire_deserialization(&request_with_fields(fields)); + } +} + +#[test] +fn empty_identifier_strings_match_current_wire_deserialization() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "", + "correlationVector": "" + }"#; + + assert_matches_current_wire_deserialization(json); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/mod.rs b/src/core/wxc_common/src/config_contract_adapters/mod.rs index 6399b0ac2..dab93cac6 100644 --- a/src/core/wxc_common/src/config_contract_adapters/mod.rs +++ b/src/core/wxc_common/src/config_contract_adapters/mod.rs @@ -1,17 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 3 introduces and tests the adapter before Phase 8 wires it into -// shadow exact-contract dispatch. +// Not yet reachable from production dispatch; see Gudge. #[cfg_attr(not(test), allow(dead_code))] pub(crate) mod v0_6; -// Phase 4 introduces and tests the adapter before Phase 8 wires it into -// shadow exact-contract dispatch. +// Not yet reachable from production dispatch; see Gudge. #[cfg_attr(not(test), allow(dead_code))] pub(crate) mod v0_7; -// Phase 5 introduces and tests the adapter before Phase 8 wires it into -// shadow exact-contract dispatch. +// Not yet reachable from production dispatch; see Gudge. #[cfg_attr(not(test), allow(dead_code))] pub(crate) mod dev; diff --git a/src/core/wxc_common/src/lib.rs b/src/core/wxc_common/src/lib.rs index 80eccb1c3..b5a32f2f7 100644 --- a/src/core/wxc_common/src/lib.rs +++ b/src/core/wxc_common/src/lib.rs @@ -26,6 +26,9 @@ pub mod script_runner; pub mod state_aware_backend; pub mod state_aware_dispatch; pub mod state_aware_request; +// Not yet reachable from production dispatch; see Gudge. +#[cfg_attr(not(test), allow(dead_code))] +pub(crate) mod state_aware_wire; pub mod telemetry; pub mod ui_policy; pub mod validator; diff --git a/src/core/wxc_common/src/state_aware_wire.rs b/src/core/wxc_common/src/state_aware_wire.rs new file mode 100644 index 000000000..3a73e0598 --- /dev/null +++ b/src/core/wxc_common/src/state_aware_wire.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Neutral pre-normalization representation shared by state-aware parser paths. + +use serde_json::Value; + +use crate::wire; + +/// Lossless intermediate state produced before runtime normalization. +pub(crate) struct StateAwareWireInput { + pub config: wire::MxcConfig, + pub experimental_raw: Option, + pub source_text: Box, +}