576 lines
19 KiB
Rust
576 lines
19 KiB
Rust
use std::collections::HashMap;
|
|
use std::net::SocketAddr;
|
|
|
|
use insta360_link_protocol::{
|
|
AudioCaptureMode, CameraModel, CompositionStyle, ExtendFunction, FilterMode, Gesture,
|
|
GestureFunction, TrackSpeed, VideoMode,
|
|
};
|
|
use insta360_link_server::{
|
|
build_router, new_mock_state, AudioRequest, CameraStateSnapshot, ImageRequest, ModeRequest,
|
|
PresetRequest, PtzRequest, SystemRequest, TrackingRequest,
|
|
};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
|
|
/// Binds an ephemeral TCP listener on 127.0.0.1:0, attaches the axum router
|
|
/// powered by `new_mock_state(model)`, spawns the server onto the tokio runtime,
|
|
/// and returns the bound socket address.
|
|
async fn start_test_server(model: CameraModel) -> SocketAddr {
|
|
let state = new_mock_state(model);
|
|
let app = build_router(state);
|
|
let listener = TcpListener::bind("127.0.0.1:0")
|
|
.await
|
|
.expect("Failed to bind ephemeral TCP listener");
|
|
let addr = listener
|
|
.local_addr()
|
|
.expect("Failed to retrieve local socket address");
|
|
|
|
tokio::spawn(async move {
|
|
let _ = axum::serve(listener, app).await;
|
|
});
|
|
|
|
addr
|
|
}
|
|
|
|
/// Sends an HTTP/1.1 request directly over a raw TCP connection (`TcpStream`),
|
|
/// eliminating external HTTP client crate requirements.
|
|
///
|
|
/// Handles chunked reading, `Content-Length` framing, and connection termination.
|
|
/// Returns `(status_code, headers, body)`.
|
|
async fn send_http_request(
|
|
addr: SocketAddr,
|
|
method: &str,
|
|
path: &str,
|
|
body: Option<&str>,
|
|
) -> (u16, HashMap<String, String>, String) {
|
|
let mut stream = TcpStream::connect(addr)
|
|
.await
|
|
.expect("Failed to connect TCP stream to test server");
|
|
|
|
let mut req = format!("{method} {path} HTTP/1.1\r\nHost: {addr}\r\nConnection: close\r\n");
|
|
if let Some(b) = body {
|
|
req.push_str("Content-Type: application/json\r\n");
|
|
req.push_str(&format!("Content-Length: {}\r\n", b.len()));
|
|
}
|
|
req.push_str("\r\n");
|
|
if let Some(b) = body {
|
|
req.push_str(b);
|
|
}
|
|
|
|
stream
|
|
.write_all(req.as_bytes())
|
|
.await
|
|
.expect("Failed to write HTTP request to TCP stream");
|
|
stream.flush().await.expect("Failed to flush TCP stream");
|
|
|
|
let mut response_buf = Vec::new();
|
|
let mut chunk = [0u8; 4096];
|
|
loop {
|
|
let n = stream
|
|
.read(&mut chunk)
|
|
.await
|
|
.expect("Failed to read HTTP response chunk from TCP stream");
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
response_buf.extend_from_slice(&chunk[..n]);
|
|
|
|
if let Some(pos) = response_buf.windows(4).position(|w| w == b"\r\n\r\n") {
|
|
let header_str = String::from_utf8_lossy(&response_buf[..pos]);
|
|
let mut content_len = None;
|
|
for line in header_str.split("\r\n") {
|
|
if let Some((k, v)) = line.split_once(':') {
|
|
if k.trim().eq_ignore_ascii_case("content-length") {
|
|
if let Ok(cl) = v.trim().parse::<usize>() {
|
|
content_len = Some(cl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(cl) = content_len {
|
|
if response_buf.len() >= pos + 4 + cl {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let header_end = response_buf
|
|
.windows(4)
|
|
.position(|w| w == b"\r\n\r\n")
|
|
.expect("Malformed HTTP response: missing header delimiter CRLF CRLF");
|
|
let header_bytes = &response_buf[..header_end];
|
|
let body_bytes = &response_buf[header_end + 4..];
|
|
|
|
let header_str = std::str::from_utf8(header_bytes).expect("Non-UTF8 HTTP headers");
|
|
let mut lines = header_str.split("\r\n");
|
|
let status_line = lines.next().expect("Missing HTTP status line");
|
|
let status_code: u16 = status_line
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.expect("Missing status code in HTTP status line")
|
|
.parse()
|
|
.expect("Invalid integer status code in HTTP response");
|
|
|
|
let mut headers = HashMap::new();
|
|
for line in lines {
|
|
if let Some((k, v)) = line.split_once(':') {
|
|
headers.insert(k.trim().to_ascii_lowercase(), v.trim().to_string());
|
|
}
|
|
}
|
|
|
|
let body_str = String::from_utf8_lossy(body_bytes).to_string();
|
|
(status_code, headers, body_str)
|
|
}
|
|
|
|
/// Helper for GET requests.
|
|
async fn send_http_get(addr: SocketAddr, path: &str) -> (u16, HashMap<String, String>, String) {
|
|
send_http_request(addr, "GET", path, None).await
|
|
}
|
|
|
|
/// Helper for JSON POST requests.
|
|
async fn send_http_json(
|
|
addr: SocketAddr,
|
|
method: &str,
|
|
path: &str,
|
|
body: &str,
|
|
) -> (u16, HashMap<String, String>, String) {
|
|
send_http_request(addr, method, path, Some(body)).await
|
|
}
|
|
|
|
#[test]
|
|
fn test_request_structs_exports() {
|
|
// Verify that all documented request structs deserialize properly
|
|
let _ptz: PtzRequest = serde_json::from_str(
|
|
r#"{"pan_arcsec": 36000, "tilt_arcsec": -18000, "zoom": 250}"#,
|
|
)
|
|
.expect("PtzRequest deserialization failed");
|
|
|
|
let _mode: ModeRequest = serde_json::from_str(
|
|
r#"{"mode": "DeskView", "pitch": -450, "zoom": 150}"#,
|
|
)
|
|
.expect("ModeRequest deserialization failed");
|
|
|
|
let _preset: PresetRequest = serde_json::from_str(
|
|
r#"{"action": "save", "index": 3}"#,
|
|
)
|
|
.expect("PresetRequest deserialization failed");
|
|
|
|
let _img: ImageRequest = serde_json::from_str(
|
|
r#"{"brightness": 75, "contrast": 65, "wb_auto": false, "wb_kelvin": 5600, "hdr": true, "filter_mode": "Warm"}"#,
|
|
)
|
|
.expect("ImageRequest deserialization failed");
|
|
|
|
let _track: TrackingRequest = serde_json::from_str(
|
|
r#"{"track_speed": "Fast", "composition_style": "WholeBody", "gesture_binding": ["Palm", "AutoFraming"]}"#,
|
|
)
|
|
.expect("TrackingRequest deserialization failed");
|
|
|
|
let _audio: AudioRequest = serde_json::from_str(
|
|
r#"{"audio_mode": "VoiceSuppression"}"#,
|
|
)
|
|
.or_else(|_| serde_json::from_str(r#"{"mode": "VoiceSuppression"}"#))
|
|
.expect("AudioRequest deserialization failed");
|
|
|
|
let _sys: SystemRequest = serde_json::from_str(
|
|
r#"{"privacy_mode": true, "horizontal_correction": true}"#,
|
|
)
|
|
.expect("SystemRequest deserialization failed");
|
|
}
|
|
|
|
/// 1. GET / returns 200 OK with text/html containing the embedded Web Remote Control UI.
|
|
#[tokio::test]
|
|
async fn test_web_remote_control_ui() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
let (status, headers, body) = send_http_get(addr, "/").await;
|
|
|
|
assert_eq!(status, 200, "GET / did not return 200 OK");
|
|
let content_type = headers
|
|
.get("content-type")
|
|
.expect("Missing Content-Type header on GET /");
|
|
assert!(
|
|
content_type.contains("text/html"),
|
|
"Content-Type should be text/html, got '{content_type}'"
|
|
);
|
|
assert!(
|
|
body.contains("<!DOCTYPE html>")
|
|
|| body.contains("<!doctype html>")
|
|
|| body.contains("<html")
|
|
|| body.contains("Insta360"),
|
|
"GET / body did not contain expected HTML UI content. Body preview: {}",
|
|
&body[..body.len().min(300)]
|
|
);
|
|
}
|
|
|
|
/// 2. GET /api/state returns 200 OK and deserializes cleanly into CameraStateSnapshot
|
|
/// with CameraModel::Link2 and zoom == 100.
|
|
#[tokio::test]
|
|
async fn test_get_api_state_initial() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
let (status, headers, body) = send_http_get(addr, "/api/state").await;
|
|
|
|
assert_eq!(status, 200, "GET /api/state did not return 200 OK");
|
|
let content_type = headers
|
|
.get("content-type")
|
|
.expect("Missing Content-Type header on GET /api/state");
|
|
assert!(
|
|
content_type.contains("application/json"),
|
|
"Content-Type should be application/json, got '{content_type}'"
|
|
);
|
|
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.unwrap_or_else(|e| panic!("Failed to deserialize CameraStateSnapshot: {e}\nPayload: {body}"));
|
|
|
|
assert_eq!(snapshot.model, CameraModel::Link2);
|
|
assert_eq!(snapshot.zoom, 100);
|
|
}
|
|
|
|
/// 3. POST /api/ptz with {"pan_arcsec": 36000, "tilt_arcsec": -18000, "zoom": 250}
|
|
/// updates pan_arcsec, tilt_arcsec, zoom, and real_zoom.
|
|
#[tokio::test]
|
|
async fn test_post_api_ptz() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/ptz",
|
|
r#"{"pan_arcsec": 36000, "tilt_arcsec": -18000, "zoom": 250}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200, "POST /api/ptz failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after PTZ update");
|
|
|
|
assert_eq!(snapshot.pan_arcsec, 36000);
|
|
assert_eq!(snapshot.tilt_arcsec, -18000);
|
|
assert_eq!(snapshot.zoom, 250);
|
|
assert_eq!(snapshot.real_zoom, 250);
|
|
}
|
|
|
|
/// 4. POST /api/mode with {"mode": "DeskView", "pitch": -450, "zoom": 150}
|
|
/// switches video_mode to VideoMode::DeskView and updates video_aux.pitch to -450.
|
|
#[tokio::test]
|
|
async fn test_post_api_mode() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/mode",
|
|
r#"{"mode": "DeskView", "pitch": -450, "zoom": 150}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200, "POST /api/mode failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after mode update");
|
|
|
|
assert_eq!(snapshot.video_mode, VideoMode::DeskView);
|
|
assert_eq!(snapshot.video_aux.pitch, -450);
|
|
}
|
|
|
|
/// 5. POST /api/image with {"brightness": 75, "contrast": 65, "wb_auto": false, "wb_kelvin": 5600, "hdr": true, "filter_mode": "Warm"}
|
|
/// updates all image parameters and sets ExtendFunction::Hdr to true.
|
|
#[tokio::test]
|
|
async fn test_post_api_image() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/image",
|
|
r#"{"brightness": 75, "contrast": 65, "wb_auto": false, "wb_kelvin": 5600, "hdr": true, "filter_mode": "Warm"}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200, "POST /api/image failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after image update");
|
|
|
|
assert_eq!(snapshot.brightness, 75);
|
|
assert_eq!(snapshot.contrast, 65);
|
|
assert_eq!(snapshot.wb_auto, false);
|
|
assert_eq!(snapshot.wb_kelvin, 5600);
|
|
assert_eq!(snapshot.filter_mode, FilterMode::Warm);
|
|
|
|
let hdr_enabled = snapshot
|
|
.extend_functions
|
|
.iter()
|
|
.any(|(f, en)| *f == ExtendFunction::Hdr && *en);
|
|
assert!(hdr_enabled, "ExtendFunction::Hdr was not set to true in snapshot.extend_functions");
|
|
}
|
|
|
|
/// 6. POST /api/tracking with {"track_speed": "Fast", "composition_style": "WholeBody", "gesture_binding": ["Palm", "AutoFraming"]}
|
|
/// updates tracking speed, composition style, and gesture bindings.
|
|
#[tokio::test]
|
|
async fn test_post_api_tracking() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/tracking",
|
|
r#"{"track_speed": "Fast", "composition_style": "WholeBody", "gesture_binding": ["Palm", "AutoFraming"]}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200, "POST /api/tracking failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after tracking update");
|
|
|
|
assert_eq!(snapshot.track_speed, TrackSpeed::Fast);
|
|
assert_eq!(snapshot.composition_style, CompositionStyle::WholeBody);
|
|
|
|
let palm_binding = snapshot
|
|
.gesture_bindings
|
|
.iter()
|
|
.find(|(g, _)| *g == Gesture::Palm);
|
|
assert_eq!(
|
|
palm_binding.map(|(_, f)| *f),
|
|
Some(GestureFunction::AutoFraming),
|
|
"Expected Palm gesture binding to be AutoFraming"
|
|
);
|
|
}
|
|
|
|
/// 7. POST /api/preset with {"action": "save", "index": 3} sets bit 3 in preset_flags.slot_mask.
|
|
#[tokio::test]
|
|
async fn test_post_api_preset() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/preset",
|
|
r#"{"action": "save", "index": 3}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200, "POST /api/preset failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after preset update");
|
|
|
|
assert_ne!(
|
|
snapshot.preset_flags.slot_mask & (1 << 3),
|
|
0,
|
|
"Bit 3 in preset_flags.slot_mask should be set after saving preset slot 3"
|
|
);
|
|
}
|
|
|
|
/// 8. POST /api/audio and POST /api/system update audio_mode, privacy_mode, and horizontal_correction.
|
|
#[tokio::test]
|
|
async fn test_post_api_audio_and_system() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
// Audio update
|
|
let mut audio_res = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/audio",
|
|
r#"{"audio_mode": "VoiceSuppression"}"#,
|
|
)
|
|
.await;
|
|
if audio_res.0 != 200 {
|
|
audio_res = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/audio",
|
|
r#"{"mode": "VoiceSuppression"}"#,
|
|
)
|
|
.await;
|
|
}
|
|
assert_eq!(audio_res.0, 200, "POST /api/audio failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after audio update");
|
|
assert_eq!(snapshot.audio_mode, AudioCaptureMode::VoiceSuppression);
|
|
|
|
// System update
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/system",
|
|
r#"{"privacy_mode": true, "horizontal_correction": true}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200, "POST /api/system failed");
|
|
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body)
|
|
.expect("Failed to deserialize CameraStateSnapshot after system update");
|
|
assert!(snapshot.privacy_mode, "privacy_mode should be true");
|
|
assert!(
|
|
snapshot.horizontal_correction,
|
|
"horizontal_correction should be true"
|
|
);
|
|
}
|
|
|
|
/// Complete end-to-end integration flow testing all 8 steps sequentially on a single
|
|
/// server instance over TCP sockets.
|
|
#[tokio::test]
|
|
async fn test_server_full_integration_flow() {
|
|
let addr = start_test_server(CameraModel::Link2).await;
|
|
|
|
// 1. GET / returns 200 OK with text/html UI
|
|
let (status, headers, body) = send_http_get(addr, "/").await;
|
|
assert_eq!(status, 200);
|
|
assert!(headers.get("content-type").unwrap().contains("text/html"));
|
|
assert!(
|
|
body.contains("<!DOCTYPE html>")
|
|
|| body.contains("<!doctype html>")
|
|
|| body.contains("<html")
|
|
|| body.contains("Insta360")
|
|
);
|
|
|
|
// 2. GET /api/state initial state
|
|
let (status, _, body) = send_http_get(addr, "/api/state").await;
|
|
assert_eq!(status, 200);
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(snapshot.model, CameraModel::Link2);
|
|
assert_eq!(snapshot.zoom, 100);
|
|
|
|
// 3. POST /api/ptz
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/ptz",
|
|
r#"{"pan_arcsec": 36000, "tilt_arcsec": -18000, "zoom": 250}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200);
|
|
|
|
let (_, _, body) = send_http_get(addr, "/api/state").await;
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(snapshot.pan_arcsec, 36000);
|
|
assert_eq!(snapshot.tilt_arcsec, -18000);
|
|
assert_eq!(snapshot.zoom, 250);
|
|
assert_eq!(snapshot.real_zoom, 250);
|
|
|
|
// 4. POST /api/mode
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/mode",
|
|
r#"{"mode": "DeskView", "pitch": -450, "zoom": 150}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200);
|
|
|
|
let (_, _, body) = send_http_get(addr, "/api/state").await;
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(snapshot.video_mode, VideoMode::DeskView);
|
|
assert_eq!(snapshot.video_aux.pitch, -450);
|
|
|
|
// 5. POST /api/image
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/image",
|
|
r#"{"brightness": 75, "contrast": 65, "wb_auto": false, "wb_kelvin": 5600, "hdr": true, "filter_mode": "Warm"}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200);
|
|
|
|
let (_, _, body) = send_http_get(addr, "/api/state").await;
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(snapshot.brightness, 75);
|
|
assert_eq!(snapshot.contrast, 65);
|
|
assert_eq!(snapshot.wb_auto, false);
|
|
assert_eq!(snapshot.wb_kelvin, 5600);
|
|
assert_eq!(snapshot.filter_mode, FilterMode::Warm);
|
|
assert!(snapshot
|
|
.extend_functions
|
|
.iter()
|
|
.any(|(f, en)| *f == ExtendFunction::Hdr && *en));
|
|
|
|
// 6. POST /api/tracking
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/tracking",
|
|
r#"{"track_speed": "Fast", "composition_style": "WholeBody", "gesture_binding": ["Palm", "AutoFraming"]}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200);
|
|
|
|
let (_, _, body) = send_http_get(addr, "/api/state").await;
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(snapshot.track_speed, TrackSpeed::Fast);
|
|
assert_eq!(snapshot.composition_style, CompositionStyle::WholeBody);
|
|
let palm_binding = snapshot
|
|
.gesture_bindings
|
|
.iter()
|
|
.find(|(g, _)| *g == Gesture::Palm);
|
|
assert_eq!(
|
|
palm_binding.map(|(_, f)| *f),
|
|
Some(GestureFunction::AutoFraming)
|
|
);
|
|
|
|
// 7. POST /api/preset
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/preset",
|
|
r#"{"action": "save", "index": 3}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200);
|
|
|
|
let (_, _, body) = send_http_get(addr, "/api/state").await;
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_ne!(snapshot.preset_flags.slot_mask & (1 << 3), 0);
|
|
|
|
// 8. POST /api/audio and POST /api/system
|
|
let mut audio_res = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/audio",
|
|
r#"{"audio_mode": "VoiceSuppression"}"#,
|
|
)
|
|
.await;
|
|
if audio_res.0 != 200 {
|
|
audio_res = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/audio",
|
|
r#"{"mode": "VoiceSuppression"}"#,
|
|
)
|
|
.await;
|
|
}
|
|
assert_eq!(audio_res.0, 200);
|
|
|
|
let (status, _, _) = send_http_json(
|
|
addr,
|
|
"POST",
|
|
"/api/system",
|
|
r#"{"privacy_mode": true, "horizontal_correction": true}"#,
|
|
)
|
|
.await;
|
|
assert_eq!(status, 200);
|
|
|
|
let (_, _, body) = send_http_get(addr, "/api/state").await;
|
|
let snapshot: CameraStateSnapshot = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(snapshot.audio_mode, AudioCaptureMode::VoiceSuppression);
|
|
assert!(snapshot.privacy_mode);
|
|
assert!(snapshot.horizontal_correction);
|
|
}
|