diff --git a/Cargo.lock b/Cargo.lock index a55d88dc..3c8dd4f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2663,6 +2663,7 @@ version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ + "encoding_rs", "memchr", "serde", ] diff --git a/crates/openplotva-app/Cargo.toml b/crates/openplotva-app/Cargo.toml index 745f505d..42dc5350 100644 --- a/crates/openplotva-app/Cargo.toml +++ b/crates/openplotva-app/Cargo.toml @@ -32,7 +32,7 @@ openplotva-web = { path = "../openplotva-web" } futures-util.workspace = true hex.workspace = true image.workspace = true -quick-xml.workspace = true +quick-xml = { workspace = true, features = ["encoding"] } rand.workspace = true redis = { workspace = true, features = ["tokio-comp"] } reqwest = { workspace = true, features = ["gzip"] } diff --git a/crates/openplotva-app/src/rates.rs b/crates/openplotva-app/src/rates.rs index 0715aa31..9a64c2b5 100644 --- a/crates/openplotva-app/src/rates.rs +++ b/crates/openplotva-app/src/rates.rs @@ -367,9 +367,7 @@ impl MarketRatesClient { let (body, stale) = self .fetch_bytes(&self.urls.cbr_xml_url, MARKET_RATES_DAILY_TTL) .await?; - let text = - String::from_utf8(body).map_err(|error| MarketRatesError::Decode(error.to_string()))?; - let parsed: CbrValCurs = quick_xml::de::from_str(&text) + let parsed: CbrValCurs = quick_xml::de::from_reader(body.as_slice()) .map_err(|error| MarketRatesError::Decode(error.to_string()))?; parsed .valutes @@ -2421,6 +2419,27 @@ mod tests { Ok(()) } + #[tokio::test] + async fn market_rates_client_parses_windows_1251_cbr_xml() + -> Result<(), Box> { + let xml = b"USD1\xC4\xEE\xEB\xEB\xE0\xF078,1234"; + let (urls, server) = spawn_market_fixture_server(vec![FixtureResponse { + status: 200, + body: xml, + }])?; + let client = market_test_client(urls)?; + let snapshot = fetch_dialog_rates(&client, parse_rates_selection("USD/RUB")).await; + assert!(snapshot.errors.is_empty(), "{:?}", snapshot.errors); + assert_eq!(snapshot.rows[0].value, 78.1234); + assert_eq!(snapshot.rows[0].timestamp.as_deref(), Some("19.06.2026")); + assert_eq!(snapshot.rows[0].source, "cbr_cross"); + let requests = server + .join() + .map_err(|_| io::Error::other("market fixture server panicked"))??; + assert_eq!(requests, vec!["/cbr".to_owned()]); + Ok(()) + } + #[tokio::test] async fn market_rates_client_fetches_cbr_cross_pairs() -> Result<(), Box> { @@ -2684,16 +2703,22 @@ mod tests { #[derive(Clone)] struct FixtureResponse { status: u16, - body: &'static str, + body: &'static [u8], } impl FixtureResponse { fn ok(body: &'static str) -> Self { - Self { status: 200, body } + Self { + status: 200, + body: body.as_bytes(), + } } fn status(status: u16, body: &'static str) -> Self { - Self { status, body } + Self { + status, + body: body.as_bytes(), + } } } @@ -2731,11 +2756,11 @@ mod tests { ); write!( stream, - "HTTP/1.1 {} OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + "HTTP/1.1 {} OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n", response.status, - response.body.len(), - response.body + response.body.len() )?; + stream.write_all(response.body)?; } Ok(requests) });