Stabilize control surface and external bridge v1
This commit is contained in:
@@ -37,7 +37,9 @@ struct HttpResponse {
|
||||
fn root_and_web_assets_target_the_versioned_api_contract() {
|
||||
let server = start_server();
|
||||
let html = send_http_request(server.local_addr(), "GET", "/", None);
|
||||
let technical_html = send_http_request(server.local_addr(), "GET", "/technical", None);
|
||||
let app_js = send_http_request(server.local_addr(), "GET", "/app.js", None);
|
||||
let technical_js = send_http_request(server.local_addr(), "GET", "/technical.js", None);
|
||||
let styles = send_http_request(server.local_addr(), "GET", "/styles.css", None);
|
||||
|
||||
assert_eq!(html.status_code, 200);
|
||||
@@ -46,19 +48,66 @@ fn root_and_web_assets_target_the_versioned_api_contract() {
|
||||
.get("content-type")
|
||||
.expect("content-type header")
|
||||
.starts_with("text/html"));
|
||||
assert!(html.body.contains("Preset Capture"));
|
||||
assert!(html.body.contains("Mapping Settings"));
|
||||
assert!(html.body.contains("<h2>Preview</h2>"));
|
||||
assert!(html.body.contains("Creative Snapshots"));
|
||||
assert!(html.body.contains("Event Stream"));
|
||||
assert!(html.body.contains("Selected Tile"));
|
||||
assert!(html.body.contains("Utilities"));
|
||||
assert!(html.body.contains("View & Output"));
|
||||
assert!(html.body.contains("Pending Transition"));
|
||||
assert!(html.body.contains("Trigger Transition"));
|
||||
assert!(html.body.contains("session-scope-label"));
|
||||
assert!(html.body.contains("Fade Go"));
|
||||
assert!(html.body.contains("Status & Eventfeed"));
|
||||
assert!(html.body.contains("Mapping Settings"));
|
||||
assert!(!html.body.contains("<h2>Groups</h2>"));
|
||||
assert!(html.body.contains("work-mode-select"));
|
||||
assert!(html.body.contains("LEDs Only"));
|
||||
assert!(!html.body.contains("preview-mode-select"));
|
||||
|
||||
assert_eq!(technical_html.status_code, 200);
|
||||
assert!(technical_html
|
||||
.body
|
||||
.contains("Infinity Vis Mapping Settings"));
|
||||
assert!(technical_html.body.contains("Backend & Output"));
|
||||
assert!(technical_html.body.contains("Node / IP Discovery"));
|
||||
assert!(technical_html.body.contains("Discover / Scan"));
|
||||
assert!(technical_html.body.contains("Node Targets"));
|
||||
assert!(technical_html.body.contains("Panel Mapping"));
|
||||
assert!(technical_html.body.contains("DDP (WLED)"));
|
||||
assert!(technical_html.body.contains("Preview Only"));
|
||||
assert!(technical_html.body.contains("Creative Surface"));
|
||||
|
||||
assert_eq!(app_js.status_code, 200);
|
||||
assert!(app_js.body.contains("/api/v1/state"));
|
||||
assert!(app_js.body.contains("/api/v1/preview"));
|
||||
assert!(app_js.body.contains("save_preset"));
|
||||
assert!(app_js.body.contains("delete_preset"));
|
||||
assert!(app_js.body.contains("save_creative_snapshot"));
|
||||
assert!(app_js.body.contains("show_control_session_required"));
|
||||
assert!(app_js.body.contains("trigger_transition"));
|
||||
assert!(app_js.body.contains("trigger_panel_test"));
|
||||
assert!(app_js.body.contains("commitState"));
|
||||
assert!(app_js.body.contains("Center Pulse"));
|
||||
assert!(app_js.body.contains("Checkerd"));
|
||||
assert!(app_js.body.contains("Wave Line"));
|
||||
assert!(app_js.body.contains("tile-led"));
|
||||
assert!(app_js.body.contains("show_event"));
|
||||
assert!(app_js.body.contains("test_edit"));
|
||||
assert!(app_js.body.contains("direct_mode_active"));
|
||||
assert!(!app_js.body.contains("Tile Colors"));
|
||||
assert!(!app_js.body.contains("Technical"));
|
||||
|
||||
assert_eq!(technical_js.status_code, 200);
|
||||
assert!(technical_js.body.contains("/api/v1/state"));
|
||||
assert!(technical_js.body.contains("set_output_backend_mode"));
|
||||
assert!(technical_js.body.contains("set_live_output_enabled"));
|
||||
assert!(technical_js.body.contains("set_output_fps"));
|
||||
assert!(technical_js.body.contains("set_node_reserved_ip"));
|
||||
assert!(technical_js.body.contains("update_panel_mapping"));
|
||||
assert!(technical_js.body.contains("/api/v1/discovery/scan"));
|
||||
assert!(technical_js.body.contains("runDiscoveryScan"));
|
||||
assert!(technical_js.body.contains("DDP (WLED)"));
|
||||
assert_eq!(styles.status_code, 200);
|
||||
assert!(styles
|
||||
.headers
|
||||
@@ -66,6 +115,9 @@ fn root_and_web_assets_target_the_versioned_api_contract() {
|
||||
.expect("content-type header")
|
||||
.starts_with("text/css"));
|
||||
assert!(styles.body.contains(".preview-grid"));
|
||||
assert!(styles.body.contains(".tile-led-ring"));
|
||||
assert!(styles.body.contains(".technical-workspace"));
|
||||
assert!(styles.body.contains(".technical-table"));
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
@@ -77,9 +129,11 @@ fn web_ui_browser_smoke_serves_shell_assets_and_stream_bootstrap() {
|
||||
let mut stream = open_websocket(server.local_addr());
|
||||
|
||||
assert_eq!(html.status_code, 200);
|
||||
assert!(html.body.contains("Infinity Vis"));
|
||||
assert!(html.body.contains("Mapping Settings"));
|
||||
assert!(html.body.contains("<h2>Preview</h2>"));
|
||||
assert!(html.body.contains("connection-pill"));
|
||||
assert!(html.body.contains("preview-grid"));
|
||||
assert!(html.body.contains("workspace-stage"));
|
||||
|
||||
let first_frame = read_websocket_text_frame(&mut stream);
|
||||
let second_frame = read_websocket_text_frame(&mut stream);
|
||||
@@ -108,10 +162,28 @@ fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
|
||||
assert_eq!(state_body["api_version"], "v1");
|
||||
assert!(state_body.get("state").is_some());
|
||||
assert!(state_body.get("preview").is_none());
|
||||
assert_eq!(
|
||||
state_body["state"]["technical"]["backend_mode"],
|
||||
"preview_only"
|
||||
);
|
||||
assert_eq!(state_body["state"]["technical"]["output_enabled"], false);
|
||||
assert_eq!(state_body["state"]["technical"]["output_fps"], 40);
|
||||
assert_eq!(
|
||||
state_body["state"]["nodes"].as_array().map(Vec::len),
|
||||
Some(6)
|
||||
);
|
||||
assert!(state_body["state"]["nodes"]
|
||||
.as_array()
|
||||
.expect("nodes array")
|
||||
.iter()
|
||||
.all(|node| node["connection"] == "offline"
|
||||
&& node["error_status"] == "preview only - live output disabled"));
|
||||
assert!(state_body["state"]["panels"]
|
||||
.as_array()
|
||||
.expect("panels array")
|
||||
.iter()
|
||||
.all(|panel| panel["connection"] == "offline"
|
||||
&& panel["driver_kind"] == "pending_validation"));
|
||||
|
||||
assert_eq!(preview.status_code, 200);
|
||||
assert_eq!(preview_body["api_version"], "v1");
|
||||
@@ -130,6 +202,110 @@ fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn technical_surface_commands_update_backend_node_targets_and_panel_mapping() {
|
||||
let server = start_server();
|
||||
|
||||
let responses = [
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_output_backend_mode","payload":{"mode":"ddp_wled"}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_live_output_enabled","payload":{"enabled":true}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_output_fps","payload":{"output_fps":55}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"set_node_reserved_ip","payload":{"node_id":"node-01","reserved_ip":"192.168.40.151"}}}"#,
|
||||
),
|
||||
send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"update_panel_mapping","payload":{"node_id":"node-01","panel_position":"top","physical_output_name":"GPIO 18","driver_kind":"gpio","driver_reference":"GPIO18","led_count":120,"direction":"reverse","color_order":"rgb","enabled":false}}}"#,
|
||||
),
|
||||
];
|
||||
|
||||
for response in responses {
|
||||
assert_eq!(response.status_code, 200);
|
||||
}
|
||||
|
||||
let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None);
|
||||
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||
|
||||
assert_eq!(state_body["state"]["technical"]["backend_mode"], "ddp_wled");
|
||||
assert_eq!(state_body["state"]["technical"]["output_enabled"], true);
|
||||
assert_eq!(state_body["state"]["technical"]["output_fps"], 55);
|
||||
assert_eq!(
|
||||
state_body["state"]["technical"]["live_status"],
|
||||
"DDP (WLED) armed - 0/6 nodes online"
|
||||
);
|
||||
assert!(state_body["state"]["nodes"]
|
||||
.as_array()
|
||||
.expect("nodes array")
|
||||
.iter()
|
||||
.any(|node| node["node_id"] == "node-01"
|
||||
&& node["reserved_ip"] == "192.168.40.151"
|
||||
&& node["error_status"] == "ddp (wled) output enabled - no live client connected"));
|
||||
assert!(state_body["state"]["panels"]
|
||||
.as_array()
|
||||
.expect("panels array")
|
||||
.iter()
|
||||
.any(|panel| panel["node_id"] == "node-01"
|
||||
&& panel["panel_position"] == "top"
|
||||
&& panel["physical_output_name"] == "GPIO 18"
|
||||
&& panel["driver_kind"] == "gpio"
|
||||
&& panel["driver_reference"] == "GPIO18"
|
||||
&& panel["led_count"] == 120
|
||||
&& panel["direction"] == "reverse"
|
||||
&& panel["color_order"] == "rgb"
|
||||
&& panel["enabled"] == false
|
||||
&& panel["error_status"] == "output disabled"));
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_scan_endpoint_returns_structured_results_and_rejects_invalid_subnets() {
|
||||
let server = start_server();
|
||||
|
||||
let invalid = send_http_request(
|
||||
server.local_addr(),
|
||||
"POST",
|
||||
"/api/v1/discovery/scan",
|
||||
Some(r#"{"subnet":"192.168.40.0"}"#),
|
||||
);
|
||||
let invalid_body: Value = serde_json::from_str(&invalid.body).expect("invalid subnet json");
|
||||
assert_eq!(invalid.status_code, 400);
|
||||
assert_eq!(invalid_body["error"]["code"], "invalid_subnet_cidr");
|
||||
|
||||
let valid = send_http_request(
|
||||
server.local_addr(),
|
||||
"POST",
|
||||
"/api/v1/discovery/scan",
|
||||
Some(r#"{"subnet":"192.168.40.0/30"}"#),
|
||||
);
|
||||
let valid_body: Value = serde_json::from_str(&valid.body).expect("valid subnet json");
|
||||
assert_eq!(valid.status_code, 200);
|
||||
assert_eq!(valid_body["api_version"], "v1");
|
||||
assert_eq!(valid_body["subnet"], "192.168.40.0/30");
|
||||
assert_eq!(valid_body["scanned_hosts"], 2);
|
||||
assert!(valid_body["results"].is_array());
|
||||
if let Some(first) = valid_body["results"]
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
{
|
||||
assert!(first.get("ip").is_some());
|
||||
assert!(first.get("reachable").is_some());
|
||||
assert!(first.get("detected_type").is_some());
|
||||
}
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_flow_updates_group_parameters_transition_and_blackout() {
|
||||
let server = start_server();
|
||||
@@ -260,6 +436,47 @@ fn presets_and_creative_snapshots_persist_across_restart() {
|
||||
let _ = std::fs::remove_file(runtime_state_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_presets_can_be_deleted_but_builtin_presets_are_protected() {
|
||||
let server = start_server();
|
||||
|
||||
let save = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"save_preset","payload":{"preset_id":"runtime_delete_me","overwrite":false}}}"#,
|
||||
);
|
||||
assert_eq!(save.status_code, 200);
|
||||
|
||||
let delete_runtime = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"delete_preset","payload":{"preset_id":"runtime_delete_me"}}}"#,
|
||||
);
|
||||
assert_eq!(delete_runtime.status_code, 200);
|
||||
|
||||
let catalog_after_delete =
|
||||
send_http_request(server.local_addr(), "GET", "/api/v1/catalog", None);
|
||||
let catalog_body: Value =
|
||||
serde_json::from_str(&catalog_after_delete.body).expect("catalog json");
|
||||
assert!(!catalog_body["presets"]
|
||||
.as_array()
|
||||
.expect("preset array")
|
||||
.iter()
|
||||
.any(|preset| preset["preset_id"] == "runtime_delete_me"));
|
||||
|
||||
let delete_builtin = send_command_json(
|
||||
server.local_addr(),
|
||||
r#"{"command":{"type":"delete_preset","payload":{"preset_id":"ocean_gradient"}}}"#,
|
||||
);
|
||||
let delete_builtin_body: Value =
|
||||
serde_json::from_str(&delete_builtin.body).expect("delete builtin json");
|
||||
assert_eq!(delete_builtin.status_code, 400);
|
||||
assert_eq!(
|
||||
delete_builtin_body["error"]["code"],
|
||||
"preset_delete_forbidden"
|
||||
);
|
||||
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_control_flows_cover_runtime_group_preset_snapshot_transition_blackout_and_eventfeed() {
|
||||
let server = start_server();
|
||||
|
||||
Reference in New Issue
Block a user