diff --git a/src/core/mxc_config_contract/src/dev/mod.rs b/src/core/mxc_config_contract/src/dev/mod.rs index 78626ba09..dddb4e263 100644 --- a/src/core/mxc_config_contract/src/dev/mod.rs +++ b/src/core/mxc_config_contract/src/dev/mod.rs @@ -103,12 +103,15 @@ pub enum Version { } } -/// The development `0.8.0-alpha` configuration contract. mod experimental; mod network; +/// The development `0.8.0-alpha` one-shot configuration contract. mod one_shot; mod primitives; +mod request; mod stable; +/// The development `0.8.0-alpha` state-aware configuration contract. +mod state_aware; pub use experimental::{ OneShotExperimental, OneShotWindowsSandbox, OneShotWslc, PortMapping, Telemetry, TestFeature, @@ -117,8 +120,27 @@ pub use experimental::{ pub use network::{DefaultNetworkPolicy, Network, NetworkEnforcementMode, NetworkProxy}; pub use one_shot::{Containment as OneShotContainment, Request as OneShotRequest}; pub use primitives::{NonEmptyString, OptionalField, True}; +pub use request::{parse_request, Request, RequestParseError}; pub use stable::{ CaptureDenials, CaptureDenialsMode, Fallback, Filesystem, LaunchMethod, Lifecycle, Lxc, Process, ProcessContainer, ProcessContainerUi, ProcessContainerUiIsolation, Seatbelt, Ui, UiClipboard, }; +pub use state_aware::{probe_containment, Containment, ContainmentProbeError}; +pub use state_aware::{probe_phase, Phase, PhaseProbeError}; +pub use state_aware::{DeprovisionExperimental, DeprovisionPhase, DeprovisionRequest}; +pub use state_aware::{ExecExperimental, ExecPhase, ExecRequest}; +pub use state_aware::{ + IsolationSessionContainment, IsolationSessionNetwork, IsolationSessionNetworkDefaultPolicy, + IsolationSessionProvision, IsolationSessionProvisionExperimental, + IsolationSessionProvisionRequest, StateAwareIsolationSession, +}; +pub use state_aware::{ProvisionPhase, ProvisionRequest}; +pub use state_aware::{StartExperimental, StartPhase, StartRequest}; +pub use state_aware::{ + StateAwareWslc, WslcContainment, WslcProvision, WslcProvisionExperimental, WslcProvisionRequest, +}; +pub use state_aware::{StopExperimental, StopPhase, StopRequest}; +pub use state_aware::{ + WindowsSandboxContainment, WindowsSandboxExperimental, WindowsSandboxProvisionRequest, +}; diff --git a/src/core/mxc_config_contract/src/dev/request.rs b/src/core/mxc_config_contract/src/dev/request.rs new file mode 100644 index 000000000..a95b8d0f6 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/request.rs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::one_shot::Request as OneShotRequest; +use super::state_aware::{probe_containment, Containment, ContainmentProbeError}; +use super::state_aware::{probe_phase, Phase, PhaseProbeError}; +use super::state_aware::{ + DeprovisionRequest, ExecRequest, ProvisionRequest, StartRequest, StopRequest, +}; + +/// A validated request for the mutable `0.8.0-alpha` development contract. +#[derive(Debug)] +pub enum Request { + /// A one-shot execution request with no lifecycle phase. + OneShot(Box), + /// A state-aware provision request selected by its containment backend. + Provision(ProvisionRequest), + /// A state-aware start request. + Start(StartRequest), + /// A state-aware process-execution request. + Exec(ExecRequest), + /// A state-aware stop request. + Stop(StopRequest), + /// A state-aware deprovision request. + Deprovision(DeprovisionRequest), +} + +/// An error encountered while selecting or deserializing a development request. +#[derive(Debug, thiserror::Error)] +pub enum RequestParseError { + /// The lifecycle phase declaration is malformed or unsupported. + #[error("Invalid phase declaration")] + Phase(#[from] PhaseProbeError), + + /// A provision request's containment declaration is malformed or unsupported. + #[error("Invalid provision containment declaration")] + Containment(#[from] ContainmentProbeError), + + /// The selected request contract rejected the complete document. + #[error("Invalid {contract} request")] + InvalidRequest { + /// Human-readable name of the selected contract. + contract: &'static str, + /// Structured deserialization error from the selected contract. + #[source] + source: serde_json::Error, + }, +} + +fn deserialize(json: &str, contract: &'static str) -> Result +where + T: serde::de::DeserializeOwned, +{ + serde_json::from_str(json) + .map_err(|source| RequestParseError::InvalidRequest { contract, source }) +} + +fn parse_provision(json: &str) -> Result { + match probe_containment(json)? { + Containment::WindowsSandbox => { + deserialize(json, "Windows Sandbox provision").map(ProvisionRequest::WindowsSandbox) + } + Containment::IsolationSession => { + deserialize(json, "IsolationSession provision").map(ProvisionRequest::IsolationSession) + } + Containment::Wslc => deserialize(json, "WSLC provision").map(ProvisionRequest::Wslc), + } +} + +/// Selects and deserializes one exact development request from raw JSON source. +/// +/// An absent `phase` selects the one-shot contract. A present phase selects its +/// corresponding state-aware contract, with provision requests additionally +/// selected by their required `containment` declaration. The selected concrete +/// request still requires the exact `0.8.0-alpha` version marker. +/// +/// # Errors +/// +/// Returns [`RequestParseError::Phase`] when the phase declaration is malformed +/// or unsupported, [`RequestParseError::Containment`] when a provision +/// containment declaration is malformed or unsupported, and +/// [`RequestParseError::InvalidRequest`] when the selected concrete contract +/// rejects the document. +pub fn parse_request(json: &str) -> Result { + match probe_phase(json)? { + None => deserialize(json, "one-shot") + .map(Box::new) + .map(Request::OneShot), + Some(Phase::Provision) => parse_provision(json).map(Request::Provision), + Some(Phase::Start) => deserialize(json, "start").map(Request::Start), + Some(Phase::Exec) => deserialize(json, "exec").map(Request::Exec), + Some(Phase::Stop) => deserialize(json, "stop").map(Request::Stop), + Some(Phase::Deprovision) => deserialize(json, "deprovision").map(Request::Deprovision), + } +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/deprovision.rs b/src/core/mxc_config_contract/src/dev/state_aware/deprovision.rs new file mode 100644 index 000000000..e47a2067b --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/deprovision.rs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::experimental::Telemetry; +use crate::dev::{OptionalField, Version}; +use serde::Deserialize; + +string_marker! { + /// The `deprovision` phase of the state-aware configuration contract. + pub struct DeprovisionPhase => "deprovision"; +} + +/// Experimental settings accepted by the `deprovision` phase. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct DeprovisionExperimental { + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// A complete state-aware `deprovision` request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct DeprovisionRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `deprovision` phase marker. + pub phase: DeprovisionPhase, + /// Identifier of the sandbox to deprovision. + pub sandbox_id: String, + + /// Optional correlation vector relayed from provision. + #[serde(default)] + pub correlation_vector: OptionalField, + + /// Optional closed post-provision experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/exec.rs b/src/core/mxc_config_contract/src/dev/state_aware/exec.rs new file mode 100644 index 000000000..6853c8db5 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/exec.rs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::experimental::Telemetry; +use crate::dev::{Network, Process}; +use crate::dev::{OptionalField, Version}; +use serde::Deserialize; + +string_marker! { + /// The `exec` phase of the state-aware configuration contract. + pub struct ExecPhase => "exec"; +} + +/// Experimental settings accepted by the `exec` phase. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExecExperimental { + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// A complete state-aware `exec` request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ExecRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `exec` phase marker. + pub phase: ExecPhase, + /// Identifier of the sandbox to execute in. + pub sandbox_id: String, + + /// Optional correlation vector relayed from provision. + #[serde(default)] + pub correlation_vector: OptionalField, + + /// Process to execute in the sandbox. + pub process: Process, + + /// Optional per-execution network settings. + #[serde(default)] + pub network: OptionalField, + + /// Optional closed exec experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/mod.rs b/src/core/mxc_config_contract/src/dev/state_aware/mod.rs new file mode 100644 index 000000000..3bf260c5a --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/mod.rs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Wire types for the mutable `0.8.0-alpha` state-aware configuration +//! contract. + +macro_rules! string_marker { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident => $wire_value:literal; + ) => { + $(#[$meta])* + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + $vis struct $name; + + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = as serde::Deserialize>::deserialize(deserializer)?; + + if value == $wire_value { + Ok(Self) + } else { + Err(serde::de::Error::unknown_variant(&value, &[$wire_value])) + } + } + } + }; +} + +mod deprovision; +mod exec; +mod phase; +mod provision; +mod start; +mod stop; + +pub use deprovision::{DeprovisionExperimental, DeprovisionPhase, DeprovisionRequest}; +pub use exec::{ExecExperimental, ExecPhase, ExecRequest}; +pub use phase::{probe_phase, Phase, PhaseProbeError}; +pub use provision::{probe_containment, Containment, ContainmentProbeError}; +pub use provision::{ + IsolationSessionContainment, IsolationSessionNetwork, IsolationSessionNetworkDefaultPolicy, + IsolationSessionProvision, IsolationSessionProvisionExperimental, + IsolationSessionProvisionRequest, StateAwareIsolationSession, +}; +pub use provision::{ProvisionPhase, ProvisionRequest}; +pub use provision::{ + StateAwareWslc, WslcContainment, WslcProvision, WslcProvisionExperimental, WslcProvisionRequest, +}; +pub use provision::{ + WindowsSandboxContainment, WindowsSandboxExperimental, WindowsSandboxProvisionRequest, +}; +pub use start::{StartExperimental, StartPhase, StartRequest}; +pub use stop::{StopExperimental, StopPhase, StopRequest}; diff --git a/src/core/mxc_config_contract/src/dev/state_aware/phase.rs b/src/core/mxc_config_contract/src/dev/state_aware/phase.rs new file mode 100644 index 000000000..439193527 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/phase.rs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// Lifecycle phase declared by a state-aware configuration request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Phase { + /// Provision a new sandbox. + Provision, + /// Start a provisioned sandbox. + Start, + /// Execute a process in a started sandbox. + Exec, + /// Stop a started sandbox without deprovisioning it. + Stop, + /// Release a provisioned sandbox and its resources. + Deprovision, +} + +impl Phase { + /// Returns the exact registered spelling of this phase. + pub const fn as_str(self) -> &'static str { + match self { + Phase::Provision => "provision", + Phase::Start => "start", + Phase::Exec => "exec", + Phase::Stop => "stop", + Phase::Deprovision => "deprovision", + } + } + + /// Looks up an exact registered state-aware phase. + /// + /// Returns `None` when `value` is not an exact supported spelling. + pub fn parse_exact(value: &str) -> Option { + match value { + "provision" => Some(Phase::Provision), + "start" => Some(Phase::Start), + "exec" => Some(Phase::Exec), + "stop" => Some(Phase::Stop), + "deprovision" => Some(Phase::Deprovision), + _ => None, + } + } +} + +/// An error encountered while probing a configuration's state-aware phase. +#[derive(Debug, thiserror::Error)] +pub enum PhaseProbeError { + /// The document is malformed, repeats `phase`, or gives it a non-string + /// value. + #[error("Invalid phase declaration")] + InvalidDeclaration(#[source] serde_json::Error), + /// The document declares a syntactically valid but unsupported phase. + /// + /// The contained string is decoded user input and must be escaped before + /// inclusion in terminal or log output. + #[error("Unsupported phase")] + UnsupportedPhase(String), +} + +use serde::{Deserialize, Deserializer}; +use std::borrow::Cow; + +#[derive(Deserialize)] +struct PhaseProbe<'a> { + #[serde(borrow, default, deserialize_with = "deserialize_present_phase")] + phase: Option>, +} + +fn deserialize_present_phase<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + Cow::deserialize(deserializer).map(Some) +} + +/// Reads the exact state-aware phase declared by a raw JSON configuration. +/// +/// Fields other than `phase` are ignored. This function validates only the +/// phase declaration; it does not validate the selected contract's complete +/// structure. +/// +/// # Errors +/// +/// Returns [`PhaseProbeError::InvalidDeclaration`] when the input is invalid +/// JSON, supplies `phase` more than once, or gives it a non-string value. +/// +/// Returns [`PhaseProbeError::UnsupportedPhase`] when `phase` is a valid +/// JSON string but is not an exact registered state-aware phase. +pub fn probe_phase(json: &str) -> Result, PhaseProbeError> { + let probe: PhaseProbe<'_> = + serde_json::from_str(json).map_err(PhaseProbeError::InvalidDeclaration)?; + + let Some(value) = probe.phase else { + return Ok(None); + }; + + Phase::parse_exact(&value) + .map(Some) + .ok_or_else(|| PhaseProbeError::UnsupportedPhase(value.into_owned())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn probe_accepts_provision() { + let json = r#"{"phase": "provision"}"#; + let phase = probe_phase(json).unwrap(); + assert_eq!(phase.unwrap().as_str(), "provision"); + } + + #[test] + fn probe_accepts_start() { + let json = r#"{"phase": "start"}"#; + let phase = probe_phase(json).unwrap(); + assert_eq!(phase.unwrap().as_str(), "start"); + } + + #[test] + fn probe_accepts_exec() { + let json = r#"{"phase": "exec"}"#; + let phase = probe_phase(json).unwrap(); + assert_eq!(phase.unwrap().as_str(), "exec"); + } + + #[test] + fn probe_accepts_stop() { + let json = r#"{"phase": "stop"}"#; + let phase = probe_phase(json).unwrap(); + assert_eq!(phase.unwrap().as_str(), "stop"); + } + + #[test] + fn probe_accepts_deprovision() { + let json = r#"{"phase": "deprovision"}"#; + let phase = probe_phase(json).unwrap(); + assert_eq!(phase.unwrap().as_str(), "deprovision"); + } + + #[test] + fn probe_ignores_unrelated_fields() { + let json = r#"{ + "phase": "provision", + "process": {"command": "echo"} + }"#; + + assert_eq!(probe_phase(json).unwrap().unwrap().as_str(), "provision"); + } + + #[test] + fn probe_accepts_missing_field() { + let json = r#"{"not_phase": "somevalue"}"#; + assert_eq!(probe_phase(json).unwrap(), None); + } + + #[test] + fn probe_rejects_multiple_fields() { + let json = r#"{"phase": "provision", "phase": "start"}"#; + let result = probe_phase(json); + assert!(matches!( + result, + Err(PhaseProbeError::InvalidDeclaration(_)) + )); + } + + #[test] + fn probe_rejects_scalar_roots() { + // Serde structs can also deserialize from positional arrays. Object-only + // root enforcement is deferred. + for json in [r#""start""#, "null", "42", "true"] { + assert!(matches!( + probe_phase(json), + Err(PhaseProbeError::InvalidDeclaration(_)) + )); + } + } + + #[test] + fn probe_rejects_trailing_json() { + let json = r#"{"phase":"provision"} {}"#; + + assert!(matches!( + probe_phase(json), + Err(PhaseProbeError::InvalidDeclaration(_)) + )); + } + + #[test] + fn probe_rejects_invalid_field_types() { + for json in [ + r#"{"phase": 0.6}"#, // number + r#"{"phase": {"major": 0, "minor": 6}}"#, // object + r#"{"phase": ["provision"]}"#, // array + r#"{"phase": null}"#, // null + r#"{"phase": true}"#, // boolean + ] { + assert!(matches!( + probe_phase(json), + Err(PhaseProbeError::InvalidDeclaration(_)) + )); + } + } + + #[test] + fn probe_reports_unrecognized_phase() { + let json = r#"{"phase": "preprovision"}"#; + let result = probe_phase(json); + assert!(matches!(result, Err(PhaseProbeError::UnsupportedPhase(_)))); + } + + #[test] + fn probe_accepts_unicode_escaped_phase() { + let json = r#"{"phase":"provis\u0069on"}"#; + + assert_eq!(probe_phase(json).unwrap(), Some(Phase::Provision)); + } + + #[test] + fn probe_reports_decoded_unsupported_phase() { + let json = r#"{"phase":"pre\u0070rovision"}"#; + + assert!(matches!( + probe_phase(json), + Err(PhaseProbeError::UnsupportedPhase(phase)) + if phase == "preprovision" + )); + } + + #[test] + fn probe_rejects_invalid_json() { + let json = r#"{"phase": "provision""#; // Missing closing brace + let result = probe_phase(json); + assert!(matches!( + result, + Err(PhaseProbeError::InvalidDeclaration(_)) + )); + } +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/provision/containment.rs b/src/core/mxc_config_contract/src/dev/state_aware/provision/containment.rs new file mode 100644 index 000000000..b57a22118 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/provision/containment.rs @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/// Containment backends that support state-aware provisioning. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Containment { + /// IsolationSession backend. + IsolationSession, + /// Windows Sandbox backend. + WindowsSandbox, + /// WSL container backend. + Wslc, +} + +impl Containment { + /// Returns the exact registered spelling of this containment. + pub const fn as_str(self) -> &'static str { + match self { + Containment::IsolationSession => "isolation_session", + Containment::WindowsSandbox => "windows_sandbox", + Containment::Wslc => "wslc", + } + } + + /// Looks up an exact registered state-aware phase. + /// + /// Returns `None` when `value` is not an exact supported spelling. + pub fn parse_exact(value: &str) -> Option { + match value { + "isolation_session" => Some(Containment::IsolationSession), + "windows_sandbox" => Some(Containment::WindowsSandbox), + "wslc" => Some(Containment::Wslc), + _ => None, + } + } +} + +/// An error encountered while probing a configuration's state-aware phase. +#[derive(Debug, thiserror::Error)] +pub enum ContainmentProbeError { + /// The document is malformed, repeats `containment`, or gives it a non-string + /// value. + #[error("Invalid containment declaration for provision phase")] + InvalidDeclaration(#[source] serde_json::Error), + /// The document declares a syntactically valid but unsupported containment value. + /// + /// The contained string is decoded user input and must be escaped before + /// inclusion in terminal or log output. + #[error("Unsupported containment for provision phase")] + UnsupportedContainment(String), +} + +use serde::Deserialize; +use std::borrow::Cow; + +#[derive(Deserialize)] +struct ContainmentProbe<'a> { + #[serde(borrow)] + containment: Cow<'a, str>, +} + +/// Reads the exact state-aware containment declared by a raw JSON configuration. +/// +/// Fields other than `containment` are ignored. This function validates only the +/// containment declaration; it does not validate the selected contract's complete +/// structure. +/// +/// # Errors +/// +/// Returns [`ContainmentProbeError::InvalidDeclaration`] when the input is invalid +/// JSON, is missing `containment`, supplies `containment` more than once, or gives it a non-string value. +/// +/// Returns [`ContainmentProbeError::UnsupportedContainment`] when `containment` is a valid +/// JSON string but is not an exact registered state-aware containment. +pub fn probe_containment(json: &str) -> Result { + let probe: ContainmentProbe<'_> = + serde_json::from_str(json).map_err(ContainmentProbeError::InvalidDeclaration)?; + Containment::parse_exact(&probe.containment).ok_or_else(|| { + ContainmentProbeError::UnsupportedContainment(probe.containment.into_owned()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn probe_accepts_isolation_session() { + let json = r#"{"containment": "isolation_session"}"#; + assert_eq!( + probe_containment(json).unwrap(), + Containment::IsolationSession + ); + } + + #[test] + fn probe_accepts_windows_sandbox() { + let json = r#"{"containment": "windows_sandbox"}"#; + assert_eq!( + probe_containment(json).unwrap(), + Containment::WindowsSandbox + ); + } + + #[test] + fn probe_accepts_wslc() { + let json = r#"{"containment": "wslc"}"#; + assert_eq!(probe_containment(json).unwrap(), Containment::Wslc); + } + + #[test] + fn probe_ignores_unrelated_fields() { + let json = r#"{ + "containment": "wslc", + "process": {"command": "echo"} + }"#; + + assert_eq!(probe_containment(json).unwrap(), Containment::Wslc); + } + + #[test] + fn probe_rejects_missing_containment() { + let json = r#"{"version": "0.8.0-alpha"}"#; + assert!(matches!( + probe_containment(json), + Err(ContainmentProbeError::InvalidDeclaration(_)) + )); + } + + #[test] + fn probe_rejects_multiple_fields() { + let json = r#"{"containment": "isolation_session", "containment": "wslc"}"#; + let result = probe_containment(json); + assert!(matches!( + result, + Err(ContainmentProbeError::InvalidDeclaration(_)) + )); + } + + #[test] + fn probe_rejects_scalar_roots() { + // Serde structs can also deserialize from positional arrays. Object-only + // root enforcement is deferred. + for json in [r#""wslc""#, "null", "42", "true"] { + assert!(matches!( + probe_containment(json), + Err(ContainmentProbeError::InvalidDeclaration(_)) + )); + } + } + + #[test] + fn probe_rejects_trailing_json() { + let json = r#"{"containment":"wslc"} {}"#; + + assert!(matches!( + probe_containment(json), + Err(ContainmentProbeError::InvalidDeclaration(_)) + )); + } + + #[test] + fn probe_rejects_invalid_field_types() { + for json in [ + r#"{"containment": 0.6}"#, // number + r#"{"containment": {"major": 0, "minor": 6}}"#, // object + r#"{"containment": ["wslc"]}"#, // array + r#"{"containment": null}"#, // null + r#"{"containment": true}"#, // boolean + ] { + assert!(matches!( + probe_containment(json), + Err(ContainmentProbeError::InvalidDeclaration(_)) + )); + } + } + + #[test] + fn probe_reports_unrecognized_containment() { + let json = r#"{"containment": "unknown"}"#; + let result = probe_containment(json); + assert!(matches!( + result, + Err(ContainmentProbeError::UnsupportedContainment(_)) + )); + } + + #[test] + fn probe_accepts_unicode_escaped_containment() { + let json = r#"{"containment":"ws\u006cc"}"#; + assert_eq!(probe_containment(json).unwrap(), Containment::Wslc); + } + + #[test] + fn probe_reports_decoded_unsupported_containment() { + let json = r#"{"containment":"unkn\u006fwn"}"#; + + assert!(matches!( + probe_containment(json), + Err(ContainmentProbeError::UnsupportedContainment(containment)) + if containment == "unknown" + )); + } + + #[test] + fn probe_rejects_invalid_json() { + let json = r#"{"containment": "wslc""#; // Missing closing brace + let result = probe_containment(json); + assert!(matches!( + result, + Err(ContainmentProbeError::InvalidDeclaration(_)) + )); + } +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/provision/isolation_session.rs b/src/core/mxc_config_contract/src/dev/state_aware/provision/isolation_session.rs new file mode 100644 index 000000000..65e549ce1 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/provision/isolation_session.rs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::state_aware::provision::ProvisionPhase; +use crate::dev::{OptionalField, Version}; +use crate::dev::{Telemetry, True}; +use serde::Deserialize; + +string_marker! { + /// The `isolation_session` containment of the state-aware configuration contract. + pub struct IsolationSessionContainment => "isolation_session"; +} + +string_marker! { + /// The exact `allow` default policy required by the IsolationSession network acknowledgment. + pub struct IsolationSessionNetworkDefaultPolicy => "allow"; +} + +/// IsolationSession settings accepted during provisioning. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct IsolationSessionProvision { + /// Optional application identifier carried by the sandbox identity. + #[serde(default)] + pub app_id: OptionalField, +} + +/// State-aware IsolationSession experimental settings. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StateAwareIsolationSession { + /// Optional provision-phase settings. + #[serde(default)] + pub provision: OptionalField, +} + +/// Experimental settings accepted by an IsolationSession provision request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct IsolationSessionProvisionExperimental { + /// Optional IsolationSession backend settings. + #[serde(rename = "isolation_session", default)] + pub isolation_session: OptionalField, + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// The exact unrestricted-network acknowledgment required when provisioning an IsolationSession. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct IsolationSessionNetwork { + /// Exact `allow` default network policy marker. + pub default_policy: IsolationSessionNetworkDefaultPolicy, + /// Required acknowledgment that local network access is allowed. + pub allow_local_network: True, +} + +/// A complete state-aware `provision` request for isolation_session +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct IsolationSessionProvisionRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `provision` phase marker. + pub phase: ProvisionPhase, + /// Exact `isolation_session` containment marker. + pub containment: IsolationSessionContainment, + /// Required unrestricted-network acknowledgment. + pub network: IsolationSessionNetwork, + /// Optional closed experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/provision/mod.rs b/src/core/mxc_config_contract/src/dev/state_aware/provision/mod.rs new file mode 100644 index 000000000..bba100ca0 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/provision/mod.rs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +string_marker! { + /// The `provision` phase of the state-aware configuration contract. + pub struct ProvisionPhase => "provision"; +} + +/// A validated provision request selected by its concrete containment backend. +#[derive(Debug)] +pub enum ProvisionRequest { + /// An IsolationSession provision request. + IsolationSession(IsolationSessionProvisionRequest), + /// A Windows Sandbox provision request. + WindowsSandbox(WindowsSandboxProvisionRequest), + /// A WSLC provision request. + Wslc(WslcProvisionRequest), +} + +mod containment; +mod isolation_session; +mod windows_sandbox; +mod wslc; + +pub use containment::{probe_containment, Containment, ContainmentProbeError}; +pub use isolation_session::{ + IsolationSessionContainment, IsolationSessionNetwork, IsolationSessionNetworkDefaultPolicy, + IsolationSessionProvision, IsolationSessionProvisionExperimental, + IsolationSessionProvisionRequest, StateAwareIsolationSession, +}; +pub use windows_sandbox::{ + WindowsSandboxContainment, WindowsSandboxExperimental, WindowsSandboxProvisionRequest, +}; +pub use wslc::{ + StateAwareWslc, WslcContainment, WslcProvision, WslcProvisionExperimental, WslcProvisionRequest, +}; diff --git a/src/core/mxc_config_contract/src/dev/state_aware/provision/windows_sandbox.rs b/src/core/mxc_config_contract/src/dev/state_aware/provision/windows_sandbox.rs new file mode 100644 index 000000000..1cd5ed0e4 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/provision/windows_sandbox.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::state_aware::provision::ProvisionPhase; +use crate::dev::{Filesystem, Telemetry}; +use crate::dev::{OptionalField, Version}; +use serde::Deserialize; + +string_marker! { + /// The `windows_sandbox` containment of the state-aware configuration contract. + pub struct WindowsSandboxContainment => "windows_sandbox"; +} + +/// Experimental settings accepted by a Windows Sandbox provision request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WindowsSandboxExperimental { + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// A complete state-aware `provision` request for windows_sandbox +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WindowsSandboxProvisionRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `provision` phase marker. + pub phase: ProvisionPhase, + /// Exact `windows_sandbox` containment marker. + pub containment: WindowsSandboxContainment, + /// Optional filesystem policy. + #[serde(default)] + pub filesystem: OptionalField, + /// Optional closed experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/provision/wslc.rs b/src/core/mxc_config_contract/src/dev/state_aware/provision/wslc.rs new file mode 100644 index 000000000..8d1847dba --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/provision/wslc.rs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::state_aware::provision::ProvisionPhase; +use crate::dev::{Filesystem, Network, Telemetry}; +use crate::dev::{OptionalField, Version}; +use serde::Deserialize; + +string_marker! { + /// The `wslc` containment of the state-aware configuration contract. + pub struct WslcContainment => "wslc"; +} + +/// WSLC settings accepted during provisioning. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WslcProvision { + /// Optional container image reference. + #[serde(default)] + pub image: OptionalField, + /// Optional path to a local container image archive. + #[serde(default)] + pub image_tar_path: OptionalField, +} + +/// State-aware WSLC experimental settings. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StateAwareWslc { + /// Optional provision-phase settings. + #[serde(default)] + pub provision: OptionalField, +} + +/// Experimental settings accepted by a WSLC provision request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WslcProvisionExperimental { + /// Optional WSLC backend settings. + #[serde(default)] + pub wslc: OptionalField, + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// A complete state-aware `provision` request for wslc +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WslcProvisionRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `provision` phase marker. + pub phase: ProvisionPhase, + /// Exact `wslc` containment marker. + pub containment: WslcContainment, + /// Optional filesystem policy fixed at provision time. + #[serde(default)] + pub filesystem: OptionalField, + /// Optional network policy fixed at provision time. + #[serde(default)] + pub network: OptionalField, + /// Optional closed experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/start.rs b/src/core/mxc_config_contract/src/dev/state_aware/start.rs new file mode 100644 index 000000000..9eb9fcf33 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/start.rs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::experimental::Telemetry; +use crate::dev::{OptionalField, Version}; +use serde::Deserialize; + +string_marker! { + /// The `start` phase of the state-aware configuration contract. + pub struct StartPhase => "start"; +} + +/// Experimental settings accepted by the `start` phase. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StartExperimental { + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// A complete state-aware `start` request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StartRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `start` phase marker. + pub phase: StartPhase, + /// Identifier returned by the provision phase. + pub sandbox_id: String, + + /// Optional correlation vector relayed from provision. + #[serde(default)] + pub correlation_vector: OptionalField, + + /// Optional closed post-provision experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/state_aware/stop.rs b/src/core/mxc_config_contract/src/dev/state_aware/stop.rs new file mode 100644 index 000000000..2b682913b --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/state_aware/stop.rs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dev::experimental::Telemetry; +use crate::dev::{OptionalField, Version}; +use serde::Deserialize; + +string_marker! { + /// The `stop` phase of the state-aware configuration contract. + pub struct StopPhase => "stop"; +} + +/// Experimental settings accepted by the `stop` phase. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StopExperimental { + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} + +/// A complete state-aware `stop` request. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct StopRequest { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// Exact development contract version. + pub version: Version, + /// Exact `stop` phase marker. + pub phase: StopPhase, + /// Identifier returned by the provision phase. + pub sandbox_id: String, + + /// Optional correlation vector relayed from provision. + #[serde(default)] + pub correlation_vector: OptionalField, + + /// Optional closed post-provision experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs index 82a7e8067..72177f216 100644 --- a/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs @@ -17,5 +17,9 @@ mod network; mod one_shot; #[path = "v0_8_0_alpha/optional_fields.rs"] mod optional_fields; +#[path = "v0_8_0_alpha/request.rs"] +mod request; #[path = "v0_8_0_alpha/seatbelt.rs"] mod seatbelt; +#[path = "v0_8_0_alpha/state_aware.rs"] +mod state_aware; diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs index 2bc26672f..d1b26989a 100644 --- a/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs @@ -6,7 +6,7 @@ use crate::common::{assert_invalid, assert_valid}; #[test] fn accepts_schema_string() { let json = r#"{ - "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "$schema": "https://example.com/one-shot.schema.json", "version": "0.8.0-alpha", "process": { "commandLine": "echo" @@ -62,7 +62,7 @@ fn accepts_all_json_comment_value_types() { #[test] fn accepts_schema_and_comment_together() { let json = r#"{ - "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "$schema": "https://example.com/one-shot.schema.json", "_comment": "This is a comment", "version": "0.8.0-alpha", "process": { @@ -76,8 +76,8 @@ fn accepts_schema_and_comment_together() { #[test] fn rejects_duplicate_schema_field() { let json = r#"{ - "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", - "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "$schema": "https://example.com/one-shot.schema.json", + "$schema": "https://example.com/one-shot.schema.json", "version": "0.8.0-alpha", "process": { "commandLine": "echo" @@ -104,7 +104,7 @@ fn rejects_duplicate_comment_field() { #[test] fn rejects_unprefixed_schema_field() { let json = r#"{ - "schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "schema": "https://example.com/one-shot.schema.json", "version": "0.8.0-alpha", "process": { "commandLine": "echo" diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs index be10ae129..28ad46e30 100644 --- a/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs @@ -2,7 +2,6 @@ // Licensed under the MIT License. use crate::common::{assert_invalid_cases, assert_valid}; -use mxc_config_contract::dev::OneShotRequest; #[test] fn accepts_empty_optional_objects() { @@ -69,7 +68,7 @@ fn accepts_absent_optional_fields() { "process": {"commandLine": "echo"} }"#; - serde_json::from_str::(json).unwrap(); + assert_valid(json); } #[test] diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/request.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/request.rs new file mode 100644 index 000000000..5fc1ebf62 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/request.rs @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use mxc_config_contract::dev::{ + parse_request, ContainmentProbeError, PhaseProbeError, ProvisionRequest, Request, + RequestParseError, +}; + +#[test] +fn no_phase_selects_one_shot_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"} + }"#; + + assert!(matches!(parse_request(json).unwrap(), Request::OneShot(_))); +} + +#[test] +fn invalid_one_shot_root_returns_invalid_request() { + let json = r#"{ + "version": "0.8.0-alpha" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::InvalidRequest { + contract: "one-shot", + .. + } + )); +} + +#[test] +fn unknown_phase_returns_phase_error() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "restart" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::Phase(PhaseProbeError::UnsupportedPhase(phase)) + if phase == "restart" + )); +} + +#[test] +fn non_string_phase_returns_phase_error() { + for phase in ["42", "true", "{}", "[]"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "containment": "wslc" + }}"# + ); + assert!(matches!( + parse_request(json.as_str()).unwrap_err(), + RequestParseError::Phase(PhaseProbeError::InvalidDeclaration(_)) + )); + } +} + +#[test] +fn missing_provision_containment_returns_containment_error() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::Containment(ContainmentProbeError::InvalidDeclaration(_)) + )); +} + +#[test] +fn unsupported_provision_containment_returns_containment_error() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "somevalue" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::Containment(ContainmentProbeError::UnsupportedContainment(_)) + )); +} + +#[test] +fn provision_phase_with_isolation_session_containment_selects_isolation_session_provision_request() +{ + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + + assert!(matches!( + parse_request(json).unwrap(), + Request::Provision(ProvisionRequest::IsolationSession(_)) + )); +} + +#[test] +fn provision_phase_with_windows_sandbox_containment_selects_windows_sandbox_provision_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox" + }"#; + + assert!(matches!( + parse_request(json).unwrap(), + Request::Provision(ProvisionRequest::WindowsSandbox(_)) + )); +} + +#[test] +fn provision_phase_with_wslc_containment_selects_wslc_provision_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc" + }"#; + + assert!(matches!( + parse_request(json).unwrap(), + Request::Provision(ProvisionRequest::Wslc(_)) + )); +} + +#[test] +fn invalid_provision_phase_isolation_session_root_returns_invalid_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::InvalidRequest { + contract: "IsolationSession provision", + .. + } + )); +} + +#[test] +fn deprovision_phase_selects_deprovision_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456" + }"#; + + assert!(matches!( + parse_request(json).unwrap(), + Request::Deprovision(_) + )); +} + +#[test] +fn invalid_deprovision_phase_root_returns_invalid_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::InvalidRequest { + contract: "deprovision", + .. + } + )); +} + +#[test] +fn exec_phase_selects_exec_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + + assert!(matches!(parse_request(json).unwrap(), Request::Exec(_))); +} + +#[test] +fn invalid_exec_phase_root_returns_invalid_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::InvalidRequest { + contract: "exec", + .. + } + )); +} + +#[test] +fn start_phase_selects_start_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456" + }"#; + + assert!(matches!(parse_request(json).unwrap(), Request::Start(_))); +} + +#[test] +fn invalid_start_phase_root_returns_invalid_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::InvalidRequest { + contract: "start", + .. + } + )); +} + +#[test] +fn stop_phase_selects_stop_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456" + }"#; + + assert!(matches!(parse_request(json).unwrap(), Request::Stop(_))); +} + +#[test] +fn invalid_stop_phase_root_returns_invalid_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop" + }"#; + + assert!(matches!( + parse_request(json).unwrap_err(), + RequestParseError::InvalidRequest { + contract: "stop", + .. + } + )); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware.rs new file mode 100644 index 000000000..4633d15a7 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[path = "state_aware/common.rs"] +mod common; +#[path = "state_aware/deprovision.rs"] +mod deprovision; +#[path = "state_aware/exec.rs"] +mod exec; +#[path = "state_aware/provision/mod.rs"] +mod provision; +#[path = "state_aware/start.rs"] +mod start; +#[path = "state_aware/stop.rs"] +mod stop; diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/common.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/common.rs new file mode 100644 index 000000000..fe67a3549 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/common.rs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use serde::de::DeserializeOwned; + +pub(crate) fn assert_valid(json: &str) +where + T: DeserializeOwned, +{ + serde_json::from_str::(json).unwrap(); +} + +pub(crate) fn assert_invalid(json: &str) +where + T: DeserializeOwned, +{ + // First ensure the test itself is valid JSON. + serde_json::from_str::(json).unwrap(); + assert!(serde_json::from_str::(json).is_err()); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/deprovision.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/deprovision.rs new file mode 100644 index 000000000..9f30ec9bc --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/deprovision.rs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::DeprovisionRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + {additional_fields} + }}"# + ) +} + +#[test] +fn accepts_minimal_deprovision_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456" + }"#; + assert_valid(json); +} + +#[test] +fn accepts_deprovision_request_with_optional_fields() { + let json = r#"{ + "$schema": "https://example.com/deprovision.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "correlationVector": "test-correlation-vector", + "experimental": { + "telemetry": { + "enabled": true + } + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_empty_deprovision_experimental_objects() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "experimental": {} + }"#; + assert_valid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "experimental": { + "telemetry": {} + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_deprovision_telemetry_enabled_values() { + for enabled in [true, false] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn accepts_empty_sandbox_id_structurally() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "" + }"#; + assert_valid(json); +} + +#[test] +fn deprovision_phase_accepts_exact_and_escaped_spelling() { + for phase in ["deprovision", "deprovis\\u0069on"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456" + }}"#, + phase + ); + assert_valid(&json); + } +} + +#[test] +fn deprovision_request_rejects_other_phases() { + for phase in ["provision", "start", "exec", "stop"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456" + }}"#, + phase + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_missing_required_deprovision_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); + + let json = r#"{ + "phase": "deprovision", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_null_required_deprovision_fields() { + let json = r#"{ + "version": null, + "phase": "deprovision", + "sandboxId": "test123456" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": null, + "sandboxId": "test123456" + }"#; + + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": null + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "sandboxId": "test123456" + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_string_sandbox_id_field() { + for sandbox_id in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": {sandbox_id} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_experimental_telemetry_enabled_field() { + for enabled in ["123", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""correlationVector": null"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_deprovision_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "unknownField": "value" + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_deprovision_experimental_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "experimental": { + "unknownField": "value" + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_deprovision_experimental_telemetry_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "deprovision", + "sandboxId": "test123456", + "experimental": { + "telemetry": { + "unknownField": "value" + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""containment": "wslc""#, + r#""process": {"commandLine": "echo"}"#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""filesystem": {}"#, + r#""fallback": {}"#, + r#""network": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""isolation_session": {}"#, + r#""wslc": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + let json = request_with_additional_fields(&format!(r#""experimental": {{{field}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_duplicate_deprovision_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "deprovision""#, + r#""sandboxId": "other""#, + r#""correlationVector": "first", "correlationVector": "second""#, + r#""experimental": {}, "experimental": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_deprovision_experimental_fields() { + for experimental in [ + r#""telemetry": {}, "telemetry": {}"#, + r#""telemetry": {"enabled": true, "enabled": false}"#, + ] { + let json = + request_with_additional_fields(&format!(r#""experimental": {{{experimental}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + let json = format!( + r#"{{ + "version": {version}, + "phase": "deprovision", + "sandboxId": "test123456" + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_phase_value() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "restart", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_invalid_optional_field_types() { + for field in ["$schema", "correlationVector"] { + for value in ["123", "true", "[]", "{}"] { + let json = request_with_additional_fields(&format!(r#""{field}": {value}"#)); + assert_invalid(&json); + } + } +} + +#[test] +fn rejects_invalid_experimental_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + let json = request_with_additional_fields(&format!(r#""experimental": {value}"#)); + assert_invalid(&json); + + let json = + request_with_additional_fields(&format!(r#""experimental": {{"telemetry": {value}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose": "test"}"#, + r#"["first", 2, false]"#, + "42", + "true", + "null", + ] { + let json = request_with_additional_fields(&format!(r#""_comment": {comment}"#)); + assert_valid(&json); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/exec.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/exec.rs new file mode 100644 index 000000000..39ae90862 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/exec.rs @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::ExecRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}}, + {additional_fields} + }}"# + ) +} + +#[test] +fn accepts_minimal_exec_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + assert_valid(json); +} + +#[test] +fn accepts_exec_request_with_optional_fields() { + let json = r#"{ + "$schema": "https://example.com/exec.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "correlationVector": "test-correlation-vector", + "process": {"commandLine": "echo"}, + "network": { + "proxy": { + "url": "http://proxy.example" + } + }, + "experimental": { + "telemetry": { + "enabled": true + } + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_empty_exec_optional_objects() { + for field in [ + r#""network": {}"#, + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_valid(&request_with_additional_fields(field)); + } +} + +#[test] +fn accepts_exec_telemetry_enabled_values() { + for enabled in [true, false] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}}, + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn accepts_empty_sandbox_id_structurally() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "", + "process": {"commandLine": "echo"} + }"#; + assert_valid(json); +} + +#[test] +fn exec_phase_accepts_exact_and_escaped_spelling() { + for phase in ["exec", "ex\\u0065c"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}} + }}"#, + phase + ); + assert_valid(&json); + } +} + +#[test] +fn exec_request_rejects_other_phases() { + for phase in ["provision", "start", "stop", "deprovision"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}} + }}"#, + phase + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_missing_required_exec_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); + + let json = r#"{ + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_null_required_exec_fields() { + let json = r#"{ + "version": null, + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": null, + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": null, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": null + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_string_sandbox_id_field() { + for sandbox_id in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": {sandbox_id}, + "process": {{"commandLine": "echo"}} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_experimental_telemetry_enabled_field() { + for enabled in ["123", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}}, + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""correlationVector": null"#, + r#""network": null"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_exec_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"}, + "unknownField": "value" + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_exec_experimental_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"}, + "experimental": { + "unknownField": "value" + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_exec_experimental_telemetry_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "test123456", + "process": {"commandLine": "echo"}, + "experimental": { + "telemetry": { + "unknownField": "value" + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_exec_network_fields() { + assert_invalid(&request_with_additional_fields( + r#""network": {"unknownField": true}"#, + )); +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""containment": "wslc""#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""filesystem": {}"#, + r#""fallback": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""isolation_session": {}"#, + r#""wslc": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + let json = request_with_additional_fields(&format!(r#""experimental": {{{field}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_duplicate_exec_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "exec""#, + r#""sandboxId": "other""#, + r#""process": {"commandLine": "echo"}"#, + r#""correlationVector": "first", "correlationVector": "second""#, + r#""network": {}, "network": {}"#, + r#""experimental": {}, "experimental": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_exec_network_fields() { + for network in [ + r#""defaultPolicy": "allow", "defaultPolicy": "block""#, + r#""proxy": {"url": "http://first"}, "proxy": {"url": "http://second"}"#, + ] { + assert_invalid(&request_with_additional_fields(&format!( + r#""network": {{{network}}}"# + ))); + } +} + +#[test] +fn rejects_duplicate_exec_experimental_fields() { + for experimental in [ + r#""telemetry": {}, "telemetry": {}"#, + r#""telemetry": {"enabled": true, "enabled": false}"#, + ] { + let json = + request_with_additional_fields(&format!(r#""experimental": {{{experimental}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + let json = format!( + r#"{{ + "version": {version}, + "phase": "exec", + "sandboxId": "test123456", + "process": {{"commandLine": "echo"}} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_phase_value() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "restart", + "sandboxId": "test123456", + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_invalid_optional_field_types() { + for field in ["$schema", "correlationVector"] { + for value in ["123", "true", "[]", "{}"] { + let json = request_with_additional_fields(&format!(r#""{field}": {value}"#)); + assert_invalid(&json); + } + } +} + +#[test] +fn rejects_invalid_experimental_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + let json = request_with_additional_fields(&format!(r#""network": {value}"#)); + assert_invalid(&json); + + let json = request_with_additional_fields(&format!(r#""experimental": {value}"#)); + assert_invalid(&json); + + let json = + request_with_additional_fields(&format!(r#""experimental": {{"telemetry": {value}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose": "test"}"#, + r#"["first", 2, false]"#, + "42", + "true", + "null", + ] { + let json = request_with_additional_fields(&format!(r#""_comment": {comment}"#)); + assert_valid(&json); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/isolation_session.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/isolation_session.rs new file mode 100644 index 000000000..a5ed4a58e --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/isolation_session.rs @@ -0,0 +1,742 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::IsolationSessionProvisionRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }}, + {additional_fields} + }}"# + ) +} + +fn request_with_network_fields(network_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": {{{network_fields}}} + }}"# + ) +} + +fn request_with_containment_value(containment: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": {containment}, + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }} + }}"# + ) +} + +#[test] +fn accepts_minimal_provision_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_provision_request_with_optional_fields() { + let json = r#"{ + "$schema": "https://example.com/provision.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "experimental": { + "telemetry": { + "enabled": true + }, + "isolation_session": { + "provision": { + "appId": "someAppId" + } + } + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_empty_provision_experimental_objects() { + for field in [ + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + r#""experimental": {"isolation_session": {}}"#, + r#""experimental": {"isolation_session": {"provision": {}}}"#, + ] { + assert_valid(&request_with_additional_fields(field)); + } +} + +#[test] +fn accepts_provision_telemetry_enabled_values() { + for enabled in [true, false] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }}, + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn provision_phase_accepts_exact_and_escaped_spelling() { + for phase in ["provision", "pr\\u006Fvision"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{phase}", + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn provision_request_rejects_other_phases() { + for phase in ["deprovision", "exec", "start", "stop"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{phase}", + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn containment_accepts_exact_and_escaped_spelling() { + for containment in ["isolation_session", "is\\u006Flation_session"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "{containment}", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn rejects_missing_required_provision_fields() { + let json = r#"{ + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow" + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_null_required_provision_fields() { + let json = r#"{ + "version": null, + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": null, + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": null, + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": null + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": null, + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": null + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_experimental_telemetry_enabled_field() { + for enabled in ["123", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }}, + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + r#""experimental": {"isolation_session": null }"#, + r#""experimental": {"isolation_session": {"provision": null }}"#, + r#""experimental": {"isolation_session": {"provision": {"appId": null }}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_provision_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "unknownField": "unknown" + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_provision_experimental_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "experimental": { + "unknownField": "unknown" + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_provision_experimental_telemetry_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "experimental": { + "telemetry": { + "unknownField": "unknown" + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_provision_experimental_isolation_session_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "experimental": { + "isolation_session": { + "unknownField": "unknown" + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_provision_experimental_isolation_session_provision_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + }, + "experimental": { + "isolation_session": { + "provision": { + "unknownField": "unknown" + } + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""process": {"commandLine": "echo"}"#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""sandboxId": "sandbox-id""#, + r#""correlationVector": "someVector""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""fallback": {}"#, + r#""filesystem": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn accepts_app_id_string_values() { + for app_id in [r#""""#, r#""someAppId""#] { + let field = format!( + r#""experimental": {{ + "isolation_session": {{ + "provision": {{"appId": {app_id}}} + }} + }}"# + ); + assert_valid(&request_with_additional_fields(&field)); + } +} + +#[test] +fn rejects_non_string_app_id() { + for app_id in ["123", "true", "false", "[]", "{}"] { + let field = format!( + r#""experimental": {{ + "isolation_session": {{ + "provision": {{"appId": {app_id}}} + }} + }}"# + ); + assert_invalid(&request_with_additional_fields(&field)); + } +} + +#[test] +fn rejects_non_isolation_session_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""wslc": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + let json = request_with_additional_fields(&format!(r#""experimental": {{{field}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_duplicate_provision_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "provision""#, + r#""containment": "isolation_session""#, + r#""network": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_provision_experimental_fields() { + for experimental in [ + r#""telemetry": {}, "telemetry": {}"#, + r#""telemetry": {"enabled": true, "enabled": false}"#, + r#""isolation_session": {}, "isolation_session": {}"#, + r#""isolation_session": { "provision": {}, "provision": {}}"#, + r#""isolation_session": { "provision": {"appId": "appIdA", "appId": "appIdB"}}"#, + ] { + let json = + request_with_additional_fields(&format!(r#""experimental": {{{experimental}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + let json = format!( + r#"{{ + "version": {version}, + "phase": "provision", + "containment": "isolation_session", + "network": {{ + "defaultPolicy": "allow", + "allowLocalNetwork": true + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_phase_value() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "startup", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": true + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_other_isolation_session_phase_keys() { + for phase in ["start", "exec", "stop", "deprovision"] { + let field = format!( + r#""experimental": {{ + "isolation_session": {{ + "{phase}": {{}} + }} + }}"# + ); + assert_invalid(&request_with_additional_fields(&field)); + } +} + +#[test] +fn rejects_non_string_schema_field() { + for value in ["123", "true", "[]", "{}"] { + let json = request_with_additional_fields(&format!(r#""$schema": {value}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_invalid_experimental_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + let json = request_with_additional_fields(&format!(r#""experimental": {value}"#)); + assert_invalid(&json); + + let json = + request_with_additional_fields(&format!(r#""experimental": {{"telemetry": {value}}}"#)); + assert_invalid(&json); + + let json = request_with_additional_fields(&format!( + r#""experimental": {{"isolation_session": {value}}}"# + )); + assert_invalid(&json); + + let json = request_with_additional_fields(&format!( + r#""experimental": {{"isolation_session": {{"provision": {value}}}}}"# + )); + assert_invalid(&json); + } +} + +#[test] +fn network_default_policy_accepts_exact_and_escaped_spelling() { + for default_policy in ["allow", "all\\u006fw"] { + let network_fields = + format!(r#""defaultPolicy": "{default_policy}", "allowLocalNetwork": true"#); + assert_valid(&request_with_network_fields(&network_fields)); + } +} + +#[test] +fn rejects_network_policy_block() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "block", + "allowLocalNetwork": true + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_allow_local_network_false() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "isolation_session", + "network": { + "defaultPolicy": "allow", + "allowLocalNetwork": false + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_invalid_network_field_types() { + for default_policy in ["123", "true", "false", "[]", "{}"] { + let network_fields = + format!(r#""defaultPolicy": {default_policy}, "allowLocalNetwork": true"#); + assert_invalid(&request_with_network_fields(&network_fields)); + } + + for allow_local_network in [r#""true""#, "123", "[]", "{}"] { + let network_fields = + format!(r#""defaultPolicy": "allow", "allowLocalNetwork": {allow_local_network}"#); + assert_invalid(&request_with_network_fields(&network_fields)); + } +} + +#[test] +fn rejects_unknown_network_fields() { + for field in [ + r#""enforcementMode": "capabilities""#, + r#""allowedHosts": []"#, + r#""blockedHosts": []"#, + r#""proxy": {"url": "http://proxy.example"}"#, + r#""unknownField": true"#, + ] { + let network_fields = + format!(r#""defaultPolicy": "allow", "allowLocalNetwork": true, {field}"#); + assert_invalid(&request_with_network_fields(&network_fields)); + } +} + +#[test] +fn rejects_duplicate_network_fields() { + for network_fields in [ + r#""defaultPolicy": "allow", "defaultPolicy": "allow", "allowLocalNetwork": true"#, + r#""defaultPolicy": "allow", "allowLocalNetwork": true, "allowLocalNetwork": true"#, + ] { + assert_invalid(&request_with_network_fields(network_fields)); + } +} + +#[test] +fn rejects_other_containment_values() { + for containment in ["windows_sandbox", "wslc", "vm", "unknown"] { + let containment = format!(r#""{containment}""#); + assert_invalid(&request_with_containment_value(&containment)); + } +} + +#[test] +fn rejects_non_string_containment_field() { + for containment in ["123", "true", "false", "[]", "{}"] { + assert_invalid(&request_with_containment_value(containment)); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose": "test"}"#, + r#"["first", 2, false]"#, + "42", + "true", + "null", + ] { + let json = request_with_additional_fields(&format!(r#""_comment": {comment}"#)); + assert_valid(&json); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/mod.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/mod.rs new file mode 100644 index 000000000..8da8f73a3 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod isolation_session; +mod windows_sandbox; +mod wslc; diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/windows_sandbox.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/windows_sandbox.rs new file mode 100644 index 000000000..652136704 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/windows_sandbox.rs @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::WindowsSandboxProvisionRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox", + {additional_fields} + }}"# + ) +} + +fn request_with_containment_value(containment: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": {containment} + }}"# + ) +} + +#[test] +fn accepts_minimal_provision_request() { + assert_valid( + r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox" + }"#, + ); +} + +#[test] +fn accepts_provision_request_with_optional_fields() { + assert_valid( + r#"{ + "$schema": "https://example.com/provision.schema.json", + "_comment": "Windows Sandbox provision", + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "windows_sandbox", + "filesystem": { + "readwritePaths": ["C:\\rw"], + "readonlyPaths": ["C:\\ro"], + "deniedPaths": ["C:\\denied"] + }, + "experimental": { + "telemetry": { + "enabled": true + } + } + }"#, + ); +} + +#[test] +fn accepts_empty_optional_objects() { + for field in [ + r#""filesystem": {}"#, + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + ] { + assert_valid(&request_with_additional_fields(field)); + } +} + +#[test] +fn accepts_telemetry_enabled_values() { + for enabled in [true, false] { + assert_valid(&request_with_additional_fields(&format!( + r#""experimental": {{"telemetry": {{"enabled": {enabled}}}}}"# + ))); + } +} + +#[test] +fn phase_accepts_exact_and_escaped_spelling() { + for phase in ["provision", "provis\\u0069on"] { + assert_valid(&format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{phase}", + "containment": "windows_sandbox" + }}"# + )); + } +} + +#[test] +fn rejects_other_phases() { + for phase in ["start", "exec", "stop", "deprovision"] { + assert_invalid(&format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{phase}", + "containment": "windows_sandbox" + }}"# + )); + } +} + +#[test] +fn containment_accepts_exact_and_escaped_spelling() { + for containment in ["windows_sandbox", "windows_sandb\\u006fx"] { + assert_valid(&request_with_containment_value(&format!( + r#""{containment}""# + ))); + } +} + +#[test] +fn rejects_other_containment_values() { + for containment in ["isolation_session", "wslc", "vm", "unknown"] { + assert_invalid(&request_with_containment_value(&format!( + r#""{containment}""# + ))); + } +} + +#[test] +fn rejects_non_string_containment_field() { + for containment in ["123", "true", "false", "[]", "{}"] { + assert_invalid(&request_with_containment_value(containment)); + } +} + +#[test] +fn rejects_missing_required_fields() { + for json in [ + r#"{"phase":"provision","containment":"windows_sandbox"}"#, + r#"{"version":"0.8.0-alpha","containment":"windows_sandbox"}"#, + r#"{"version":"0.8.0-alpha","phase":"provision"}"#, + ] { + assert_invalid(json); + } +} + +#[test] +fn rejects_null_required_fields() { + for json in [ + r#"{"version":null,"phase":"provision","containment":"windows_sandbox"}"#, + r#"{"version":"0.8.0-alpha","phase":null,"containment":"windows_sandbox"}"#, + r#"{"version":"0.8.0-alpha","phase":"provision","containment":null}"#, + ] { + assert_invalid(json); + } +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + assert_invalid(&format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "containment": "windows_sandbox" + }}"# + )); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""filesystem": null"#, + r#""filesystem": {"readwritePaths": null}"#, + r#""filesystem": {"readonlyPaths": null}"#, + r#""filesystem": {"deniedPaths": null}"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_fields_at_each_object_level() { + for field in [ + r#""unknownField": true"#, + r#""filesystem": {"unknownField": true}"#, + r#""experimental": {"unknownField": true}"#, + r#""experimental": {"telemetry": {"unknownField": true}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""process": {"commandLine": "echo"}"#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""sandboxId": "sandbox-id""#, + r#""correlationVector": "vector""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""fallback": {}"#, + r#""network": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""isolation_session": {}"#, + r#""wslc": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(&format!( + r#""experimental": {{{field}}}"# + ))); + } +} + +#[test] +fn rejects_duplicate_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "provision""#, + r#""containment": "windows_sandbox""#, + r#""filesystem": {}, "filesystem": {}"#, + r#""experimental": {}, "experimental": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_nested_fields() { + for field in [ + r#""filesystem": {"readwritePaths": [], "readwritePaths": []}"#, + r#""filesystem": {"readonlyPaths": [], "readonlyPaths": []}"#, + r#""filesystem": {"deniedPaths": [], "deniedPaths": []}"#, + r#""experimental": {"telemetry": {}, "telemetry": {}}"#, + r#""experimental": {"telemetry": {"enabled": true, "enabled": false}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + assert_invalid(&format!( + r#"{{ + "version": {version}, + "phase": "provision", + "containment": "windows_sandbox" + }}"# + )); + } +} + +#[test] +fn rejects_unknown_phase_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "phase": "restart", + "containment": "windows_sandbox" + }"#, + ); +} + +#[test] +fn rejects_non_string_schema_field() { + for value in ["123", "true", "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""$schema": {value}"# + ))); + } +} + +#[test] +fn rejects_invalid_optional_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + for field in [ + format!(r#""filesystem": {value}"#), + format!(r#""experimental": {value}"#), + format!(r#""experimental": {{"telemetry": {value}}}"#), + ] { + assert_invalid(&request_with_additional_fields(&field)); + } + } +} + +#[test] +fn rejects_invalid_filesystem_field_types() { + for path_field in ["readwritePaths", "readonlyPaths", "deniedPaths"] { + for value in [r#""path""#, "123", "true", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""filesystem": {{"{path_field}": {value}}}"# + ))); + } + + for item in ["123", "true", "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""filesystem": {{"{path_field}": [{item}]}}"# + ))); + } + } +} + +#[test] +fn rejects_non_boolean_telemetry_enabled() { + for value in ["123", r#""true""#, "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""experimental": {{"telemetry": {{"enabled": {value}}}}}"# + ))); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose":"test"}"#, + r#"["first",2,false]"#, + "42", + "true", + "null", + ] { + assert_valid(&request_with_additional_fields(&format!( + r#""_comment": {comment}"# + ))); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/wslc.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/wslc.rs new file mode 100644 index 000000000..86231957f --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/provision/wslc.rs @@ -0,0 +1,441 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::WslcProvisionRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc", + {additional_fields} + }}"# + ) +} + +fn request_with_containment_value(containment: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": {containment} + }}"# + ) +} + +#[test] +fn accepts_minimal_provision_request() { + assert_valid( + r#"{ + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc" + }"#, + ); +} + +#[test] +fn accepts_provision_request_with_optional_fields() { + assert_valid( + r#"{ + "$schema": "https://example.com/provision.schema.json", + "_comment": "WSLC provision", + "version": "0.8.0-alpha", + "phase": "provision", + "containment": "wslc", + "filesystem": { + "readwritePaths": ["C:\\rw"], + "readonlyPaths": ["C:\\ro"], + "deniedPaths": ["C:\\denied"] + }, + "network": { + "defaultPolicy": "block" + }, + "experimental": { + "telemetry": { + "enabled": true + }, + "wslc": { + "provision": { + "image": "alpine:latest", + "imageTarPath": "C:\\images\\alpine.tar" + } + } + } + }"#, + ); +} + +#[test] +fn accepts_empty_optional_objects() { + for field in [ + r#""filesystem": {}"#, + r#""network": {}"#, + r#""experimental": {}"#, + r#""experimental": {"telemetry": {}}"#, + r#""experimental": {"wslc": {}}"#, + r#""experimental": {"wslc": {"provision": {}}}"#, + ] { + assert_valid(&request_with_additional_fields(field)); + } +} + +#[test] +fn accepts_telemetry_enabled_values() { + for enabled in [true, false] { + assert_valid(&request_with_additional_fields(&format!( + r#""experimental": {{"telemetry": {{"enabled": {enabled}}}}}"# + ))); + } +} + +#[test] +fn accepts_provision_string_values() { + for (field, value) in [ + ("image", r#""""#), + ("image", r#""alpine:latest""#), + ("imageTarPath", r#""""#), + ("imageTarPath", r#""C:\\images\\alpine.tar""#), + ] { + assert_valid(&request_with_additional_fields(&format!( + r#""experimental": {{"wslc": {{"provision": {{"{field}": {value}}}}}}}"# + ))); + } +} + +#[test] +fn phase_accepts_exact_and_escaped_spelling() { + for phase in ["provision", "provis\\u0069on"] { + assert_valid(&format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{phase}", + "containment": "wslc" + }}"# + )); + } +} + +#[test] +fn rejects_other_phases() { + for phase in ["start", "exec", "stop", "deprovision"] { + assert_invalid(&format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{phase}", + "containment": "wslc" + }}"# + )); + } +} + +#[test] +fn containment_accepts_exact_and_escaped_spelling() { + for containment in ["wslc", "ws\\u006cc"] { + assert_valid(&request_with_containment_value(&format!( + r#""{containment}""# + ))); + } +} + +#[test] +fn rejects_other_containment_values() { + for containment in ["isolation_session", "windows_sandbox", "vm", "unknown"] { + assert_invalid(&request_with_containment_value(&format!( + r#""{containment}""# + ))); + } +} + +#[test] +fn rejects_non_string_containment_field() { + for containment in ["123", "true", "false", "[]", "{}"] { + assert_invalid(&request_with_containment_value(containment)); + } +} + +#[test] +fn rejects_missing_required_fields() { + for json in [ + r#"{"phase":"provision","containment":"wslc"}"#, + r#"{"version":"0.8.0-alpha","containment":"wslc"}"#, + r#"{"version":"0.8.0-alpha","phase":"provision"}"#, + ] { + assert_invalid(json); + } +} + +#[test] +fn rejects_null_required_fields() { + for json in [ + r#"{"version":null,"phase":"provision","containment":"wslc"}"#, + r#"{"version":"0.8.0-alpha","phase":null,"containment":"wslc"}"#, + r#"{"version":"0.8.0-alpha","phase":"provision","containment":null}"#, + ] { + assert_invalid(json); + } +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + assert_invalid(&format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "containment": "wslc" + }}"# + )); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""filesystem": null"#, + r#""filesystem": {"readwritePaths": null}"#, + r#""filesystem": {"readonlyPaths": null}"#, + r#""filesystem": {"deniedPaths": null}"#, + r#""network": null"#, + r#""network": {"defaultPolicy": null}"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + r#""experimental": {"wslc": null}"#, + r#""experimental": {"wslc": {"provision": null}}"#, + r#""experimental": {"wslc": {"provision": {"image": null}}}"#, + r#""experimental": {"wslc": {"provision": {"imageTarPath": null}}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_fields_at_each_object_level() { + for field in [ + r#""unknownField": true"#, + r#""filesystem": {"unknownField": true}"#, + r#""network": {"unknownField": true}"#, + r#""experimental": {"unknownField": true}"#, + r#""experimental": {"telemetry": {"unknownField": true}}"#, + r#""experimental": {"wslc": {"unknownField": true}}"#, + r#""experimental": {"wslc": {"provision": {"unknownField": true}}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_other_wslc_phase_keys() { + for phase in ["start", "exec", "stop", "deprovision"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""experimental": {{"wslc": {{"{phase}": {{}}}}}}"# + ))); + } +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""process": {"commandLine": "echo"}"#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""sandboxId": "sandbox-id""#, + r#""correlationVector": "vector""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""fallback": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_foreign_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""isolation_session": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(&format!( + r#""experimental": {{{field}}}"# + ))); + } +} + +#[test] +fn rejects_duplicate_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "provision""#, + r#""containment": "wslc""#, + r#""filesystem": {}, "filesystem": {}"#, + r#""network": {}, "network": {}"#, + r#""experimental": {}, "experimental": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_nested_fields() { + for field in [ + r#""filesystem": {"readwritePaths": [], "readwritePaths": []}"#, + r#""network": {"defaultPolicy": "block", "defaultPolicy": "allow"}"#, + r#""experimental": {"telemetry": {}, "telemetry": {}}"#, + r#""experimental": {"wslc": {}, "wslc": {}}"#, + r#""experimental": {"wslc": {"provision": {}, "provision": {}}}"#, + r#""experimental": {"wslc": {"provision": {"image": "a", "image": "b"}}}"#, + r#""experimental": {"wslc": {"provision": {"imageTarPath": "a", "imageTarPath": "b"}}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + assert_invalid(&format!( + r#"{{ + "version": {version}, + "phase": "provision", + "containment": "wslc" + }}"# + )); + } +} + +#[test] +fn rejects_unknown_phase_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "phase": "restart", + "containment": "wslc" + }"#, + ); +} + +#[test] +fn rejects_non_string_schema_field() { + for value in ["123", "true", "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""$schema": {value}"# + ))); + } +} + +#[test] +fn rejects_invalid_optional_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + for field in [ + format!(r#""filesystem": {value}"#), + format!(r#""network": {value}"#), + format!(r#""experimental": {value}"#), + format!(r#""experimental": {{"telemetry": {value}}}"#), + format!(r#""experimental": {{"wslc": {value}}}"#), + format!(r#""experimental": {{"wslc": {{"provision": {value}}}}}"#), + ] { + assert_invalid(&request_with_additional_fields(&field)); + } + } +} + +#[test] +fn rejects_invalid_filesystem_field_types() { + for path_field in ["readwritePaths", "readonlyPaths", "deniedPaths"] { + for value in [r#""path""#, "123", "true", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""filesystem": {{"{path_field}": {value}}}"# + ))); + } + + for item in ["123", "true", "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""filesystem": {{"{path_field}": [{item}]}}"# + ))); + } + } +} + +#[test] +fn rejects_invalid_network_field_types() { + for field in [ + r#""defaultPolicy": 123"#, + r#""enforcementMode": true"#, + r#""allowedHosts": "host""#, + r#""blockedHosts": {}"#, + r#""allowLocalNetwork": "true""#, + r#""proxy": "proxy""#, + ] { + assert_invalid(&request_with_additional_fields(&format!( + r#""network": {{{field}}}"# + ))); + } +} + +#[test] +fn rejects_non_string_provision_fields() { + for field in ["image", "imageTarPath"] { + for value in ["123", "true", "false", "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""experimental": {{"wslc": {{"provision": {{"{field}": {value}}}}}}}"# + ))); + } + } +} + +#[test] +fn rejects_non_boolean_telemetry_enabled() { + for value in ["123", r#""true""#, "[]", "{}"] { + assert_invalid(&request_with_additional_fields(&format!( + r#""experimental": {{"telemetry": {{"enabled": {value}}}}}"# + ))); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose":"test"}"#, + r#"["first",2,false]"#, + "42", + "true", + "null", + ] { + assert_valid(&request_with_additional_fields(&format!( + r#""_comment": {comment}"# + ))); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/start.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/start.rs new file mode 100644 index 000000000..1ef14af56 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/start.rs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::StartRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + {additional_fields} + }}"# + ) +} + +#[test] +fn accepts_minimal_start_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456" + }"#; + assert_valid(json); +} + +#[test] +fn accepts_start_request_with_optional_fields() { + let json = r#"{ + "$schema": "https://example.com/start.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "correlationVector": "test-correlation-vector", + "experimental": { + "telemetry": { + "enabled": true + } + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_empty_start_experimental_objects() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "experimental": {} + }"#; + assert_valid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "experimental": { + "telemetry": {} + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_start_telemetry_enabled_values() { + for enabled in [true, false] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn accepts_empty_sandbox_id_structurally() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "" + }"#; + assert_valid(json); +} + +#[test] +fn start_phase_accepts_exact_and_escaped_spelling() { + for phase in ["start", "st\\u0061rt"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456" + }}"#, + phase + ); + assert_valid(&json); + } +} + +#[test] +fn start_request_rejects_other_phases() { + for phase in ["provision", "exec", "stop", "deprovision"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456" + }}"#, + phase + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_missing_required_start_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); + + let json = r#"{ + "phase": "start", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_null_required_start_fields() { + let json = r#"{ + "version": null, + "phase": "start", + "sandboxId": "test123456" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": null, + "sandboxId": "test123456" + }"#; + + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": null + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "sandboxId": "test123456" + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_string_sandbox_id_field() { + for sandbox_id in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": {sandbox_id} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_experimental_telemetry_enabled_field() { + for enabled in ["123", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""correlationVector": null"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_start_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "unknownField": "value" + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_start_experimental_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "experimental": { + "unknownField": "value" + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_start_experimental_telemetry_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "start", + "sandboxId": "test123456", + "experimental": { + "telemetry": { + "unknownField": "value" + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""containment": "wslc""#, + r#""process": {"commandLine": "echo"}"#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""filesystem": {}"#, + r#""fallback": {}"#, + r#""network": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""isolation_session": {}"#, + r#""wslc": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + let json = request_with_additional_fields(&format!(r#""experimental": {{{field}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_duplicate_start_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "start""#, + r#""sandboxId": "other""#, + r#""correlationVector": "first", "correlationVector": "second""#, + r#""experimental": {}, "experimental": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_start_experimental_fields() { + for experimental in [ + r#""telemetry": {}, "telemetry": {}"#, + r#""telemetry": {"enabled": true, "enabled": false}"#, + ] { + let json = + request_with_additional_fields(&format!(r#""experimental": {{{experimental}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + let json = format!( + r#"{{ + "version": {version}, + "phase": "start", + "sandboxId": "test123456" + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_phase_value() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "restart", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_invalid_optional_field_types() { + for field in ["$schema", "correlationVector"] { + for value in ["123", "true", "[]", "{}"] { + let json = request_with_additional_fields(&format!(r#""{field}": {value}"#)); + assert_invalid(&json); + } + } +} + +#[test] +fn rejects_invalid_experimental_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + let json = request_with_additional_fields(&format!(r#""experimental": {value}"#)); + assert_invalid(&json); + + let json = + request_with_additional_fields(&format!(r#""experimental": {{"telemetry": {value}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose": "test"}"#, + r#"["first", 2, false]"#, + "42", + "true", + "null", + ] { + let json = request_with_additional_fields(&format!(r#""_comment": {comment}"#)); + assert_valid(&json); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/stop.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/stop.rs new file mode 100644 index 000000000..26c372abd --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/state_aware/stop.rs @@ -0,0 +1,416 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::common::{ + assert_invalid as assert_invalid_request, assert_valid as assert_valid_request, +}; +use mxc_config_contract::dev::StopRequest; + +fn assert_valid(json: &str) { + assert_valid_request::(json); +} + +fn assert_invalid(json: &str) { + assert_invalid_request::(json); +} + +fn request_with_additional_fields(additional_fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + {additional_fields} + }}"# + ) +} + +#[test] +fn accepts_minimal_stop_request() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456" + }"#; + assert_valid(json); +} + +#[test] +fn accepts_stop_request_with_optional_fields() { + let json = r#"{ + "$schema": "https://example.com/stop.schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "correlationVector": "test-correlation-vector", + "experimental": { + "telemetry": { + "enabled": true + } + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_empty_stop_experimental_objects() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "experimental": {} + }"#; + assert_valid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "experimental": { + "telemetry": {} + } + }"#; + assert_valid(json); +} + +#[test] +fn accepts_stop_telemetry_enabled_values() { + for enabled in [true, false] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_valid(&json); + } +} + +#[test] +fn accepts_empty_sandbox_id_structurally() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "" + }"#; + assert_valid(json); +} + +#[test] +fn stop_phase_accepts_exact_and_escaped_spelling() { + for phase in ["stop", "sto\\u0070"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456" + }}"#, + phase + ); + assert_valid(&json); + } +} + +#[test] +fn stop_request_rejects_other_phases() { + for phase in ["provision", "start", "exec", "deprovision"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "{}", + "sandboxId": "test123456" + }}"#, + phase + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_missing_required_stop_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); + + let json = r#"{ + "phase": "stop", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_null_required_stop_fields() { + let json = r#"{ + "version": null, + "phase": "stop", + "sandboxId": "test123456" + }"#; + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": null, + "sandboxId": "test123456" + }"#; + + assert_invalid(json); + + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": null + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_phase_field() { + for phase in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": {phase}, + "sandboxId": "test123456" + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_string_sandbox_id_field() { + for sandbox_id in ["123", "true", "false", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": {sandbox_id} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_experimental_telemetry_enabled_field() { + for enabled in ["123", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }} + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_null_optional_fields() { + for field in [ + r#""$schema": null"#, + r#""correlationVector": null"#, + r#""experimental": null"#, + r#""experimental": {"telemetry": null}"#, + r#""experimental": {"telemetry": {"enabled": null}}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_unknown_stop_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "unknownField": "value" + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_stop_experimental_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "experimental": { + "unknownField": "value" + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_unknown_stop_experimental_telemetry_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "stop", + "sandboxId": "test123456", + "experimental": { + "telemetry": { + "unknownField": "value" + } + } + }"#; + assert_invalid(json); +} + +#[test] +fn rejects_forbidden_fields() { + for field in [ + r#""containment": "wslc""#, + r#""process": {"commandLine": "echo"}"#, + r#""lifecycle": {}"#, + r#""containerId": "container-id""#, + r#""processContainer": {}"#, + r#""appContainer": {}"#, + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}"#, + r#""filesystem": {}"#, + r#""fallback": {}"#, + r#""network": {}"#, + r#""ui": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + assert_invalid(&request_with_additional_fields(field)); + } +} + +#[test] +fn rejects_backend_experimental_fields() { + for field in [ + r#""test": {}"#, + r#""windows_sandbox": {}"#, + r#""isolation_session": {}"#, + r#""wslc": {}"#, + r#""seatbelt": {}"#, + r#""macos_sandbox": {}"#, + ] { + let json = request_with_additional_fields(&format!(r#""experimental": {{{field}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_duplicate_stop_fields() { + for fields in [ + r#""$schema": "first", "$schema": "second""#, + r#""_comment": "first", "_comment": "second""#, + r#""version": "0.8.0-alpha""#, + r#""phase": "stop""#, + r#""sandboxId": "other""#, + r#""correlationVector": "first", "correlationVector": "second""#, + r#""experimental": {}, "experimental": {}"#, + ] { + assert_invalid(&request_with_additional_fields(fields)); + } +} + +#[test] +fn rejects_duplicate_stop_experimental_fields() { + for experimental in [ + r#""telemetry": {}, "telemetry": {}"#, + r#""telemetry": {"enabled": true, "enabled": false}"#, + ] { + let json = + request_with_additional_fields(&format!(r#""experimental": {{{experimental}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn rejects_invalid_version_field() { + for version in [ + r#""0.7.0-alpha""#, + r#""0.8.0""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + ] { + let json = format!( + r#"{{ + "version": {version}, + "phase": "stop", + "sandboxId": "test123456" + }}"# + ); + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_phase_value() { + let json = r#"{ + "version": "0.8.0-alpha", + "phase": "restart", + "sandboxId": "test123456" + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_invalid_optional_field_types() { + for field in ["$schema", "correlationVector"] { + for value in ["123", "true", "[]", "{}"] { + let json = request_with_additional_fields(&format!(r#""{field}": {value}"#)); + assert_invalid(&json); + } + } +} + +#[test] +fn rejects_invalid_experimental_object_types() { + // Positional-array rejection is intentionally out of scope. + for value in [r#""invalid""#, "123", "true"] { + let json = request_with_additional_fields(&format!(r#""experimental": {value}"#)); + assert_invalid(&json); + + let json = + request_with_additional_fields(&format!(r#""experimental": {{"telemetry": {value}}}"#)); + assert_invalid(&json); + } +} + +#[test] +fn accepts_comment_value_types() { + for comment in [ + r#""comment""#, + r#"{"purpose": "test"}"#, + r#"["first", 2, false]"#, + "42", + "true", + "null", + ] { + let json = request_with_additional_fields(&format!(r#""_comment": {comment}"#)); + assert_valid(&json); + } +}