fix: Audit-Befunde in Protokoll-Parsing, Error-Handling und Eingabe-Validierung
BragiResponse-Felder korrekt zugeordnet (endpoint=raw[1], command=raw[2], status=raw[3]) gemäß Protokoll-Doku. PropertyNotSupported durch DeviceError ersetzt, parse_response_validated in device.rs aktiviert, flush() mit Iterationslimit gegen Endlosschleifen, Sidetone-Range per clap validiert statt clamp, JSON-Escaping im hidpp-battery-waybar.sh, udev auf uaccess umgestellt. 52 Tests grün.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// ABOUTME: Unit-Tests für CLI-Argument-Parsing und Validierung.
|
||||
// ABOUTME: Testet clap-Constraints wie Sidetone Range-Validierung.
|
||||
|
||||
use clap::Parser;
|
||||
use corsairctl::cli::Cli;
|
||||
|
||||
/// Hilfsfunktion: Parst CLI-Argumente aus einem String-Slice.
|
||||
fn parse_args(args: &[&str]) -> Result<Cli, clap::Error> {
|
||||
Cli::try_parse_from(args)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidetone_rejects_negative() {
|
||||
let result = parse_args(&["corsairctl", "sidetone", "--", "-1"]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidetone_rejects_above_23() {
|
||||
let result = parse_args(&["corsairctl", "sidetone", "24"]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidetone_accepts_valid() {
|
||||
let result = parse_args(&["corsairctl", "sidetone", "15"]);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidetone_accepts_zero() {
|
||||
let result = parse_args(&["corsairctl", "sidetone", "0"]);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidetone_accepts_max() {
|
||||
let result = parse_args(&["corsairctl", "sidetone", "23"]);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidetone_without_value_is_read_mode() {
|
||||
let result = parse_args(&["corsairctl", "sidetone"]);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -37,9 +37,8 @@ fn format_sidetone_value() {
|
||||
#[test]
|
||||
fn format_info_values() {
|
||||
let result = output::format_info(0x1B1C, 0x0A69, 1797, 0);
|
||||
assert!(result.contains("0x1B1C"));
|
||||
assert!(result.contains("0x0A69"));
|
||||
assert!(result.contains("1797.0"));
|
||||
let expected = "Vendor ID: 0x1B1C\nProduct ID: 0x0A69\nFirmware: 1797.0";
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
// Waybar-JSON Tests
|
||||
|
||||
@@ -68,6 +68,23 @@ fn battery_promille_to_percent_fractional() {
|
||||
assert!((battery_promille_to_percent(735) - 73.5).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_status_icons() {
|
||||
// Jede Variante muss ein nicht-leeres Icon liefern
|
||||
assert!(!BatteryStatus::Offline.icon().is_empty());
|
||||
assert!(!BatteryStatus::Discharging.icon().is_empty());
|
||||
assert!(!BatteryStatus::Low.icon().is_empty());
|
||||
assert!(!BatteryStatus::Charging.icon().is_empty());
|
||||
assert!(!BatteryStatus::FullyCharged.icon().is_empty());
|
||||
assert!(!BatteryStatus::Unknown(0xFF).icon().is_empty());
|
||||
|
||||
// Discharging und Low teilen das gleiche Icon
|
||||
assert_eq!(BatteryStatus::Discharging.icon(), BatteryStatus::Low.icon());
|
||||
|
||||
// Charging und FullyCharged haben unterschiedliche Icons
|
||||
assert_ne!(BatteryStatus::Charging.icon(), BatteryStatus::FullyCharged.icon());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_ids_match_protocol() {
|
||||
assert_eq!(Property::BatteryLevel.id(), 0x0F);
|
||||
|
||||
+66
-20
@@ -46,22 +46,23 @@ fn build_packet_total_size() {
|
||||
|
||||
#[test]
|
||||
fn parse_response_extracts_fields() {
|
||||
// Simulierte Response: [marker, status, endpoint, command, data0, data1, ...]
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xE8, 0x03];
|
||||
// Simulierte Response: [report_type, endpoint, command, status, data0, data1, ...]
|
||||
// Laut Protokoll-Doku: raw[0]=0x01 Report-Typ, raw[1]=Endpoint, raw[2]=Command, raw[3]=Status
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
|
||||
assert_eq!(resp.status, STATUS_OK);
|
||||
assert_eq!(resp.endpoint, ENDPOINT_HEADSET);
|
||||
assert_eq!(resp.endpoint, RESPONSE_ENDPOINT_HEADSET);
|
||||
assert_eq!(resp.command, CMD_GET);
|
||||
assert_eq!(resp.status, STATUS_OK);
|
||||
assert_eq!(resp.data[0], 0xE8);
|
||||
assert_eq!(resp.data[1], 0x03);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_value_u16_little_endian() {
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xE8, 0x03];
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
@@ -73,7 +74,7 @@ fn parse_response_value_u16_little_endian() {
|
||||
|
||||
#[test]
|
||||
fn parse_response_value_u8() {
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0x03];
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
@@ -82,31 +83,31 @@ fn parse_response_value_u8() {
|
||||
|
||||
#[test]
|
||||
fn parse_response_error_f0() {
|
||||
let raw = vec![0x02, 0xF0, 0x09, 0x02];
|
||||
let raw = vec![0x01, 0xF0, CMD_GET, STATUS_OK];
|
||||
|
||||
let result = parse_response(&raw);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::PropertyNotSupported { .. }
|
||||
));
|
||||
match result.unwrap_err() {
|
||||
CorsairError::DeviceError { status } => assert_eq!(status, 0xF0),
|
||||
other => panic!("Erwarteter DeviceError, bekommen: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_error_f1() {
|
||||
let raw = vec![0x02, 0xF1, 0x09, 0x02];
|
||||
let raw = vec![0x01, 0xF1, CMD_GET, STATUS_OK];
|
||||
|
||||
let result = parse_response(&raw);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::PropertyNotSupported { .. }
|
||||
));
|
||||
match result.unwrap_err() {
|
||||
CorsairError::DeviceError { status } => assert_eq!(status, 0xF1),
|
||||
other => panic!("Erwarteter DeviceError, bekommen: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_too_short() {
|
||||
let raw = vec![0x02, 0x00];
|
||||
let raw = vec![0x01, 0x00];
|
||||
|
||||
let result = parse_response(&raw);
|
||||
assert!(result.is_err());
|
||||
@@ -118,7 +119,7 @@ fn parse_response_too_short() {
|
||||
|
||||
#[test]
|
||||
fn value_u16_on_empty_data_returns_error() {
|
||||
let raw = vec![0x02, 0x00, 0x09, 0x02]; // Keine Daten-Bytes
|
||||
let raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK]; // Keine Daten-Bytes
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
|
||||
let result = resp.value_u16();
|
||||
@@ -132,7 +133,7 @@ fn value_u16_on_empty_data_returns_error() {
|
||||
#[test]
|
||||
fn battery_level_full_is_1000_promille() {
|
||||
// Battery Level = 1000 (voll) → 0xE8, 0x03 LE
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xE8, 0x03];
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
@@ -143,10 +144,55 @@ fn battery_level_full_is_1000_promille() {
|
||||
#[test]
|
||||
fn battery_level_half_is_500_promille() {
|
||||
// Battery Level = 500 (50%) → 0xF4, 0x01 LE
|
||||
let mut raw = vec![0x02, 0x00, 0x09, 0x02, 0xF4, 0x01];
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK, 0xF4, 0x01];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response(&raw).unwrap();
|
||||
let promille = resp.value_u16().unwrap();
|
||||
assert_eq!(promille, 500);
|
||||
}
|
||||
|
||||
// --- Schritt 4: parse_response_validated Tests ---
|
||||
|
||||
#[test]
|
||||
fn parse_response_validated_rejects_wrong_endpoint() {
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_RECEIVER, CMD_GET, STATUS_OK, 0x00];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let result = parse_response_validated(&raw, Some(RESPONSE_ENDPOINT_HEADSET), None);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::UnexpectedResponse { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_validated_rejects_wrong_command() {
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_SET, STATUS_OK, 0x00];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let result = parse_response_validated(&raw, None, Some(CMD_GET));
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
CorsairError::UnexpectedResponse { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_response_validated_accepts_matching() {
|
||||
let mut raw = vec![0x01, RESPONSE_ENDPOINT_HEADSET, CMD_GET, STATUS_OK, 0xE8, 0x03];
|
||||
raw.resize(REPORT_SIZE, 0x00);
|
||||
|
||||
let resp = parse_response_validated(
|
||||
&raw,
|
||||
Some(RESPONSE_ENDPOINT_HEADSET),
|
||||
Some(CMD_GET),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.endpoint, RESPONSE_ENDPOINT_HEADSET);
|
||||
assert_eq!(resp.command, CMD_GET);
|
||||
assert_eq!(resp.status, STATUS_OK);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user