summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Guschin <guschin@altlinux.org>2026-10-30 05:15:56 +0400
committerAndrew Guschin <guschin@altlinux.org>2026-10-30 05:15:56 +0400
commit3c8b130be89fbb0e58e2ee23f07ad56a3b9de2e7 (patch)
treed0e5d447f7324a2793bc6975ed57f172624d6311
parent7999fab25c5172207ee41e38732f4923565ff6b6 (diff)
Reject streamed diagnostic error documents and record VM qualification
-rw-r--r--README.md6
-rw-r--r--docs/implementation-review.md18
-rw-r--r--docs/validation/linux-vm-continued.txt115
-rw-r--r--src/diagnostics.rs31
-rw-r--r--src/diagnostics/error_document.rs170
-rw-r--r--tests/diagnostics_acceptance.rs36
-rw-r--r--tests/vm_diagnostics_acceptance.py7
7 files changed, 366 insertions, 17 deletions
diff --git a/README.md b/README.md
index ecd2380..829bfb1 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Roentgen is a Linux CLI/TUI for importing 3x-ui subscription link lists and controlling the local machine's proxy intent through a root-owned controller. The implementation follows [.scratch/linux-first-release/spec.md](.scratch/linux-first-release/spec.md).
-This is an implementation candidate. Full native-TUN, boot, SSH authentication and recovery acceptance on the required current ALT Xray package is incomplete. The available ALT test VM and its refreshed Sisyphus repository currently supply Xray 26.3.27. Activation rejects that release before changing enabled intent. This build currently gates activation on the researched Xray 26.10.29 configuration contract. That gate is not a claim that the full release acceptance matrix has passed.
+This is an implementation candidate. Full native-TUN forwarding and managed-probe lifecycle acceptance on the required current ALT Xray package is incomplete. The supplied VM passed diagnostics transport, terminal authentication, enabled/blocked reboot protection, and recovery checks. The available ALT test VM and its refreshed Sisyphus repository currently supply Xray 26.3.27. Activation rejects that release before changing enabled intent. This build currently gates activation on the researched Xray 26.10.29 configuration contract. That gate is not a claim that the full release acceptance matrix has passed.
## Build
@@ -50,6 +50,8 @@ Explicit recovery persists disabled intent before releasing owned capture and pr
Run tests only on the supplied disposable ALT VM. Compile acceptance executables with `cargo test --features linux-runtime --no-run`; copy the executable and test artifacts to the VM or run Cargo there. CLI tests support `ROENTGEN_TEST_BINARY` when the executable is installed at another path.
-`tests/controller_acceptance.rs` exercises the public controller with real TOML/SQLite persistence and HTTP/runtime boundary fixtures. `tests/cli_acceptance.rs` exercises the executable interface. `tests/vm_guard_acceptance.py` checks real kernel traffic through the guard CLI. `tests/vm_controller_acceptance.py` checks the CLI/system-bus/polkit boundary, using temporary integration fixtures when the packaged Xray is too old for product installation. The Python checks refuse to run outside the authorized `altctlrs` VM and require its explicit VM-only marker.
+`tests/controller_acceptance.rs` exercises the public controller with real TOML/SQLite persistence and HTTP/runtime boundary fixtures. `tests/cli_acceptance.rs` exercises the executable interface. `tests/diagnostics_acceptance.rs` uses the public controller and CLI with verified TLS, the production probe configuration, and a real non-TUN Xray Trojan path. Its VM wrapper is `tests/vm_diagnostics_acceptance.py`. `tests/vm_guard_acceptance.py` checks real kernel traffic through the guard CLI. `tests/vm_boot_acceptance.py` checks enabled/blocked reboot ordering, controller crashes, and recovery from a stopped controller. `tests/vm_controller_acceptance.py` checks the CLI/system-bus/polkit boundary, using temporary integration fixtures when the packaged Xray is too old for product installation. The Python checks refuse to run outside the authorized `altctlrs` VM and require its explicit VM-only marker.
The VM guard checks use a temporary network namespace and veth pair, never the workstation's network. Preserve a powered-off checkpoint before running them, and restore it after validation. Credentials and VM reset settings belong outside this repository.
+
+The latest VM run passed 20 Rust acceptance tests and 29 VM workflow checks. See the [review and qualification limits](docs/implementation-review.md) and [captured output](docs/validation/linux-vm-continued.txt). The VM checkpoint was restored after validation.
diff --git a/docs/implementation-review.md b/docs/implementation-review.md
index 0bf25c0..99ecf29 100644
--- a/docs/implementation-review.md
+++ b/docs/implementation-review.md
@@ -22,12 +22,24 @@ The specification review found five correctness issues:
- Manual diagnostic requests ignored provider cooldowns. Retry-After now prevents further diagnostic requests to that provider, including manual Ping and Speed.
- Competing HTTP and measurement timeouts could turn a completed duration-limited Speed observation into a generic error. Diagnostics now use one measurement deadline and report consumed bytes when the duration cap ends a nonempty measurement.
-Spec findings: five correctness issues, corrected. The stale-selection forwarding path was the most serious finding. VM CLI regressions cover failed preflight retries and startup with stale capture and unset selection. The probe TLS changes and duration/rate-limit behavior still require end-to-end validation with the required current Xray runtime.
+Spec findings: five correctness issues, corrected. The stale-selection forwarding path was the most serious finding. VM CLI regressions cover failed preflight retries and startup with stale capture and unset selection. The continued validation below covers probe TLS transport and duration/rate-limit behavior using non-TUN fixtures. Qualification of the full managed lifecycle on the required current Xray runtime remains pending.
## Validation boundary
After the user's VM-only instruction, all test execution used the supplied ALT VM. Compilation and formatting used the local checkout. The VM checkpoint precedes package/test preparation and is restored after validation.
-The current package available from that VM's refreshed Sisyphus repository is Xray 26.3.27. The implementation rejects activation with it. The tests therefore distinguish actual guard/controller/polkit/editor/recovery behavior from unverified native-TUN and diagnostic operation with the researched newer Xray contract. The product installer is not counted as a successful installation in this environment; system-bus tests stage their integration fixtures explicitly.
+The current package available from that VM's refreshed Sisyphus repository is Xray 26.3.27. The implementation rejects activation with it. The tests distinguish actual guard/controller/polkit/editor/recovery behavior and fixture-backed diagnostics from unverified native-TUN forwarding and managed-probe lifecycle operation with the researched newer Xray contract. The product installer is not counted as a successful installation in this environment; system-bus tests stage their integration fixtures explicitly.
-The final available suite passed on 2026-10-30: 10 Rust CLI/controller acceptance tests and 21 VM workflow checks. The [captured output](validation/linux-vm.txt) records the tested executable digest and each result. Checks include TCP and UDP DNS destination policy, IPv4/IPv6 blocking, retained incoming connections, denial/allow authorization, ordinary-user TUI restoration, editor privacy, failed-preflight retries, stale capture with unset selection, malformed TOML recovery, missing durable state, and uninstall retention/purge.
+The initial available suite passed on 2026-10-30: 10 Rust CLI/controller acceptance tests and 21 VM workflow checks. The [captured output](validation/linux-vm.txt) records the tested executable digest and each result. Checks include TCP and UDP DNS destination policy, IPv4/IPv6 blocking, retained incoming connections, denial/allow authorization, ordinary-user TUI restoration, editor privacy, failed-preflight retries, stale capture with unset selection, malformed TOML recovery, missing durable state, and uninstall retention/purge.
+
+## Continued implementation and review
+
+Independent standards and specification reviews compared candidate `7999fab` against `fc3c1d51b1be9db6ba6ad286584fbc34bb305f38`. Standards found no actionable issues. Spec found one P2 issue: a JSON error document larger than the diagnostic prefix could count as a successful Speed download. A bounded-memory scanner now checks top-level error markers throughout consumed response chunks. Controller acceptance checks cover long error values, late and escaped error keys, legitimate nested payload fields, and HTML after long leading whitespace. All pass.
+
+The continued work also corrected diagnostic CLI exit status after provider rejection and stale successful observations after probe startup failure. Probe sessions now use the runtime boundary, and both production and acceptance fixtures share the authenticated HTTPS proxy configuration. Networking units use a shared dependency drop-in so the guard starts first.
+
+The final available suite passed on 2026-10-30: 20 Rust acceptance tests and 29 VM workflow checks. The [continued captured output](validation/linux-vm-continued.txt) records the tested executable digest and results. All tests ran only on the supplied ALT VM. The diagnostic fixtures used its distribution-owned Xray 26.3.27 in non-TUN mode, a temporary trusted CA, an authenticated HTTPS proxy, a real Trojan TLS connection, and a local HTTPS provider. Checks covered TLS failures without direct fallback, redirects and error documents, byte/time limits, both Retry-After header forms, cancellation, scheduling, and CLI failures. Temporary certificates and trust anchors were removed afterward.
+
+System checks used actual terminal polkit/PAM authentication with wrong and correct temporary fixture passwords, cancellation, and noninteractive denial. A real VM reboot retained enabled/blocked intent, and the guard became active before NetworkManager. SIGKILL and SIGSTOP checks retained protection; explicit recovery persisted disabled intent before releasing it. The reboot fixture deliberately had no selected configuration, so it does not qualify successful native-TUN forwarding after reboot.
+
+The required Xray 26.10.29 distribution package was unavailable in the refreshed VM repository. Production compatibility requirements remain unchanged. Full native-TUN forwarding, successful managed-probe startup, and the product installation lifecycle on that package remain unqualified. The VM checkpoint was restored after this run.
diff --git a/docs/validation/linux-vm-continued.txt b/docs/validation/linux-vm-continued.txt
new file mode 100644
index 0000000..2683a56
--- /dev/null
+++ b/docs/validation/linux-vm-continued.txt
@@ -0,0 +1,115 @@
+Candidate SHA-256: 991a9548054179821055a063dd8134637b727fb06108c62bcbd2214015cebf03
+xray-core-26.3.27-alt2.x86_64
+libnftnl-1.3.2-alt1.x86_64
+libmnl-1.0.5-alt2.d24c886.x86_64
+Running: cli_acceptance-8ac5b3df5e4cd10a
+
+running 2 tests
+test subscription_url_cannot_be_passed_on_the_command_line ... ok
+test help_describes_the_controller_commands_without_contacting_services ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
+Running: roentgen-2037e66e0f348630
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
+Running: diagnostics_acceptance-f430a3a9a6964e8f
+
+running 10 tests
+test cancelling_a_bus_diagnostic_stops_it_without_changing_proxy_intent ... ok
+test cli_exits_unsuccessfully_when_the_provider_rate_limits_a_diagnostic ... ok
+test controller_ping_uses_the_selected_trojan_path_with_verified_tls_without_enabling ... ok
+test controller_records_a_failed_check_when_a_previously_working_probe_cannot_start ... ok
+test controller_rejects_an_untrusted_target_or_local_proxy_without_direct_fallback ... ok
+test controller_rejects_redirects_oversized_ping_and_error_downloads ... ok
+test controller_rejects_streamed_json_errors_without_misclassifying_payload_fields ... ok
+test controller_speed_stops_at_the_first_byte_or_duration_limit ... ok
+test provider_retry_after_blocks_manual_ping_and_speed_for_both_header_forms ... ok
+test scheduled_checks_use_only_selection_never_run_speed_and_do_not_overlap ... ok
+
+test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.82s
+
+Running: controller_acceptance-7693ccc2191fcbcc
+
+running 8 tests
+test denied_operations_leave_durable_state_unchanged ... ok
+test importing_requires_explicit_selection_and_enabling ... ok
+test mixed_base64_import_keeps_all_rows_and_prohibits_invalid_selection ... ok
+test losing_a_selection_keeps_enabled_intent_and_blocks_without_replacement ... ok
+test failed_fetch_preserves_selection_but_an_empty_received_list_clears_it ... ok
+test label_changes_keep_identity_but_duplicate_candidates_clear_selection ... ok
+test persisted_intent_restores_under_protection_after_controller_restart ... ok
+test explicit_unset_and_deletion_never_disable_or_select_another_entry ... ok
+
+test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
+
+Running: roentgen-0d6b8b6ef4b4cfd3
+
+running 0 tests
+
+test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
+
+Running: vm_guard_acceptance.py
+{
+ "passed": [
+ "private IPv4/IPv6 destinations stay direct on ports 53 and 18080",
+ "public IPv4/IPv6 destinations are blocked on ports 53 and 18080",
+ "UDP DNS follows the same IPv4/IPv6 private/public destination rule",
+ "classified incoming TCP connection survives guard replacement and toggles",
+ "preexisting outgoing TCP connection receives no established-flow exemption",
+ "explicit disabled guard restores ordinary direct routing"
+ ]
+}
+Running: vm_boot_acceptance.py hang
+{
+ "passed": [
+ "guard remains armed while controller is stopped",
+ "explicit recovery terminates a hung controller and persists disabled intent before releasing protection"
+ ],
+ "recovery_seconds": 0.03
+}
+Running: vm_boot_acceptance.py prepare-boot
+Enabled/blocked boot fixture prepared; reboot the VM next.
+Rebooting the authorized ALT VM.
+Running: vm_boot_acceptance.py verify-boot
+{
+ "passed": [
+ "normal reboot restores enabled/blocked intent",
+ "guard becomes active before NetworkManager",
+ "SIGKILL does not release guard protection",
+ "systemd restarts the controller without disabling intent"
+ ],
+ "guard_active_us": 5099817,
+ "network_manager_active_us": 5246105
+}
+Running: vm_controller_acceptance.py
+{
+ "passed": [
+ "real HTTP import remains disabled/unselected and public CLI output omits credentials",
+ "noninteractive non-root mutation is denied with durable state unchanged",
+ "terminal polkit prompts under an ordinary user; cancellation leaves selection, intent and generation unchanged",
+ "terminal polkit rejects a wrong password and authorizes the real mutation after valid VM-fixture PAM authentication",
+ "polkit site allow policy authorizes the actual ordinary bus caller",
+ "mixed update retains ordered invalid/unsupported rows and normalized identity",
+ "whole-document failure preserves selection while received empty list clears it",
+ "unsupported packaged Xray activation is rejected before enabled intent changes",
+ "service restart retains explicit selection and disabled intent",
+ "editor receives literal arguments and a user-owned private snapshot that is removed afterward",
+ "ordinary-user TUI renders, accepts navigation/help/quit, restores terminal modes, and preserves service intent",
+ "restart and scheduled retry preserve blocked intent after failed compatibility checks",
+ "restart with unset selection removes stale capture and keeps TUN forwarding blocked",
+ "explicit recovery persists disabled intent even with malformed editable TOML",
+ "subscription deletion clears selection without replacement",
+ "missing durable enabled state fails closed; only explicit recovery initializes disabled state",
+ "uninstall retains saved data by default, explicit purge removes it, and system Xray remains installed"
+ ]
+}
+ALL AVAILABLE VM TESTS PASSED: 20 Rust acceptance tests and 29 VM workflow checks
+
+Post-validation restoration: roentgen-before-implementation checkpoint restored.
+SSH/PVE guest identity verified by the reset helper.
+Read-only guest checks confirmed no product executable, configuration, state,
+fixture accounts, roentgen nftables table, or test-installed Xray package remained.
diff --git a/src/diagnostics.rs b/src/diagnostics.rs
index f6ed82a..d8f8620 100644
--- a/src/diagnostics.rs
+++ b/src/diagnostics.rs
@@ -1,3 +1,5 @@
+mod error_document;
+
use crate::model::Settings;
use anyhow::{Result, anyhow, ensure};
use futures_util::TryStreamExt;
@@ -113,6 +115,7 @@ pub async fn measure(
let mut reader = tokio_util::io::StreamReader::new(stream);
let mut buffer = [0u8; 16384];
let mut prefix = Vec::new();
+ let mut error_document = error_document::ErrorDocument::default();
let mut bytes = 0;
let reason;
loop {
@@ -144,8 +147,23 @@ pub async fn measure(
break;
}
if prefix.len() < 512 {
- prefix.extend_from_slice(&buffer[..chunk.min(512 - prefix.len())]);
+ let mut inspect = &buffer[..chunk];
+ if prefix.is_empty() {
+ inspect = inspect.trim_ascii_start();
+ while inspect
+ .first()
+ .is_some_and(|byte| matches!(byte, 0xef | 0xbb | 0xbf))
+ {
+ inspect = inspect[1..].trim_ascii_start();
+ }
+ }
+ prefix.extend_from_slice(&inspect[..inspect.len().min(512 - prefix.len())]);
}
+ error_document.feed(&buffer[..chunk]);
+ ensure!(
+ !error_document.confirmed_error(),
+ "Diagnostic endpoint returned an error document"
+ );
bytes += chunk as u64;
if !speed {
ensure!(bytes <= cap, "Ping body exceeds 4 KiB");
@@ -158,17 +176,8 @@ pub async fn measure(
!prefix.starts_with("<!doctype html") && !prefix.starts_with("<html"),
"Diagnostic endpoint returned an HTML document"
);
- let json_error = serde_json::from_str::<serde_json::Value>(&prefix)
- .ok()
- .is_some_and(|body| {
- ["error", "errors"].iter().any(|key| {
- body.get(key).is_some_and(|value| {
- !value.is_null() && value != false && value != 0 && value != ""
- })
- })
- });
ensure!(
- !json_error,
+ !error_document.is_error(),
"Diagnostic endpoint returned an error document"
);
ensure!(!speed || bytes > 0, "Speed response contained no data");
diff --git a/src/diagnostics/error_document.rs b/src/diagnostics/error_document.rs
new file mode 100644
index 0000000..d3aab03
--- /dev/null
+++ b/src/diagnostics/error_document.rs
@@ -0,0 +1,170 @@
+//! Recognize top-level JSON error markers without buffering response values.
+//! This classifies provider error documents; it does not validate arbitrary JSON.
+
+#[derive(Default)]
+pub(super) struct ErrorDocument {
+ started: bool,
+ finished: bool,
+ depth: usize,
+ string: bool,
+ escaped: bool,
+ expect_key: bool,
+ key: Option<Vec<u8>>,
+ error_key: bool,
+ value: Option<ErrorValue>,
+ detected: bool,
+}
+
+enum ErrorValue {
+ Start,
+ String,
+ Container(u8),
+ Literal(Vec<u8>),
+}
+
+impl ErrorDocument {
+ pub fn feed(&mut self, bytes: &[u8]) {
+ for &byte in bytes {
+ if self.finished || self.detected {
+ break;
+ }
+ if !self.started {
+ // JSON producers sometimes include a UTF-8 BOM.
+ if byte.is_ascii_whitespace() || matches!(byte, 0xef | 0xbb | 0xbf) {
+ continue;
+ }
+ self.started = true;
+ if byte != b'{' {
+ self.finished = true;
+ continue;
+ }
+ self.depth = 1;
+ self.expect_key = true;
+ continue;
+ }
+ self.inspect_value(byte);
+ if self.detected {
+ break;
+ }
+ if self.string {
+ if let Some(key) = &mut self.key {
+ if key.len() < 64 {
+ key.push(byte);
+ } else {
+ self.key = None;
+ }
+ }
+ if self.escaped {
+ self.escaped = false;
+ continue;
+ }
+ if byte == b'\\' {
+ self.escaped = true;
+ continue;
+ }
+ if byte == b'"' {
+ self.string = false;
+ if let Some(key) = self.key.take() {
+ self.error_key =
+ serde_json::from_slice::<String>(&key)
+ .ok()
+ .is_some_and(|key| {
+ key.eq_ignore_ascii_case("error")
+ || key.eq_ignore_ascii_case("errors")
+ });
+ self.expect_key = false;
+ }
+ }
+ continue;
+ }
+ match byte {
+ b'"' => {
+ self.string = true;
+ if self.depth == 1 && self.expect_key {
+ self.key = Some(vec![b'"']);
+ }
+ }
+ b'{' | b'[' => self.depth = self.depth.saturating_add(1),
+ b'}' | b']' => {
+ self.depth = self.depth.saturating_sub(1);
+ if self.depth == 0 {
+ self.finished = true;
+ }
+ }
+ b':' if self.depth == 1 && self.error_key => {
+ self.value = Some(ErrorValue::Start);
+ self.error_key = false;
+ }
+ b',' if self.depth == 1 => {
+ self.expect_key = true;
+ self.error_key = false;
+ }
+ _ => {}
+ }
+ }
+ }
+
+ fn inspect_value(&mut self, byte: u8) {
+ let Some(value) = self.value.take() else {
+ return;
+ };
+ self.value = match value {
+ ErrorValue::Start if byte.is_ascii_whitespace() => Some(ErrorValue::Start),
+ ErrorValue::Start => match byte {
+ b'"' => Some(ErrorValue::String),
+ b'[' => Some(ErrorValue::Container(b']')),
+ b'{' => Some(ErrorValue::Container(b'}')),
+ b'n' | b'f' | b'0' | b'-' => Some(ErrorValue::Literal(vec![byte])),
+ _ => {
+ self.detected = true;
+ None
+ }
+ },
+ ErrorValue::String => {
+ self.detected = byte != b'"';
+ None
+ }
+ ErrorValue::Container(end) if byte.is_ascii_whitespace() => {
+ Some(ErrorValue::Container(end))
+ }
+ ErrorValue::Container(end) => {
+ self.detected = byte != end;
+ None
+ }
+ ErrorValue::Literal(mut bytes) => {
+ if byte.is_ascii_whitespace() || matches!(byte, b',' | b'}') {
+ self.detected = literal_is_error(&bytes);
+ None
+ } else if bytes.len() < 64 {
+ bytes.push(byte);
+ Some(ErrorValue::Literal(bytes))
+ } else {
+ self.detected = true;
+ None
+ }
+ }
+ };
+ }
+
+ pub fn is_error(&self) -> bool {
+ self.detected
+ || match &self.value {
+ Some(ErrorValue::Literal(bytes)) => literal_is_error(bytes),
+ // An incomplete value of a declared error field is not a successful download.
+ Some(_) => true,
+ None => false,
+ }
+ }
+
+ pub fn confirmed_error(&self) -> bool {
+ self.detected
+ }
+}
+
+fn literal_is_error(bytes: &[u8]) -> bool {
+ match serde_json::from_slice::<serde_json::Value>(bytes) {
+ Ok(serde_json::Value::Null | serde_json::Value::Bool(false)) => false,
+ Ok(serde_json::Value::Number(number)) => number.as_f64() != Some(0.0),
+ _ => true,
+ }
+}
diff --git a/tests/diagnostics_acceptance.rs b/tests/diagnostics_acceptance.rs
index f1db2de..0dcba04 100644
--- a/tests/diagnostics_acceptance.rs
+++ b/tests/diagnostics_acceptance.rs
@@ -603,3 +603,39 @@ async fn cancelling_a_bus_diagnostic_stops_it_without_changing_proxy_intent() {
.all(|entry| entry.speed_bytes_per_second.is_none())
);
}
+
+#[tokio::test]
+async fn controller_rejects_streamed_json_errors_without_misclassifying_payload_fields() {
+ let mut context = Context::open().await;
+ for path in ["/long-error", "/late-error", "/escaped-error"] {
+ context.settings("/empty", path, 1, 1).await;
+ let error = context
+ .controller
+ .diagnostic(&Caller::root(), None, true)
+ .await
+ .unwrap_err();
+ assert!(
+ error.to_string().contains("error document"),
+ "Long provider error was not recognized for {path}: {error}"
+ );
+ }
+ context.settings("/empty", "/late-html", 1, 1).await;
+ assert!(
+ context
+ .controller
+ .diagnostic(&Caller::root(), None, true)
+ .await
+ .unwrap_err()
+ .to_string()
+ .contains("HTML")
+ );
+ context.settings("/empty", "/json-data", 1, 1).await;
+ let observation = context
+ .controller
+ .diagnostic(&Caller::root(), None, true)
+ .await
+ .unwrap();
+ assert!(observation.success);
+ assert!(observation.bytes > 30000);
+ assert_eq!(observation.stop_reason, "complete");
+}
diff --git a/tests/vm_diagnostics_acceptance.py b/tests/vm_diagnostics_acceptance.py
index 1984585..001f1a3 100644
--- a/tests/vm_diagnostics_acceptance.py
+++ b/tests/vm_diagnostics_acceptance.py
@@ -75,9 +75,14 @@ def main(binary):
self.send_response(206); self.end_headers(); self.wfile.write(b'partial'); return
self.send_response(200)
if self.path == '/error': self.send_header('Content-Type', 'application/problem+json')
- if self.path == '/json-error': self.send_header('Content-Type', 'application/json')
+ if self.path in ['/json-error', '/long-error', '/late-error', '/escaped-error', '/json-data']: self.send_header('Content-Type', 'application/json')
body = b''
if self.path in ['/error', '/json-error']: body = b'{"error":"fixture-provider-error"}'
+ if self.path == '/long-error': body = json.dumps({'error':'x'*30000}).encode()
+ if self.path == '/late-error': body = json.dumps({'metadata':'x'*30000, 'error':'provider failure'}).encode()
+ if self.path == '/escaped-error': body = b'{"\\u0065rror":"' + b'x'*20000 + b'"}'
+ if self.path == '/json-data': body = json.dumps({'data':{'error':'nested payload'},'metadata':'x'*30000 + '"error":not a field', 'error':None,'errors':[]}).encode()
+ if self.path == '/late-html': body = b' ' * 30000 + b'<html>provider error</html>'
if self.path == '/html': body = b'<html>unexpected page</html>'
if self.path == '/oversize': body = b'x' * 4097
if self.path == '/large': body = b'x' * (8 * 1048576)