Skip to main content

nautilus_binance/common/
urls.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! URL resolution helpers for Binance API endpoints.
17
18use super::{
19    consts::{
20        BINANCE_FUTURES_COIN_DEMO_HTTP_URL, BINANCE_FUTURES_COIN_DEMO_WS_URL,
21        BINANCE_FUTURES_COIN_HTTP_URL, BINANCE_FUTURES_COIN_TESTNET_HTTP_URL,
22        BINANCE_FUTURES_COIN_TESTNET_WS_URL, BINANCE_FUTURES_COIN_WS_URL,
23        BINANCE_FUTURES_USD_DEMO_HTTP_URL, BINANCE_FUTURES_USD_DEMO_WS_URL,
24        BINANCE_FUTURES_USD_HTTP_URL, BINANCE_FUTURES_USD_TESTNET_HTTP_URL,
25        BINANCE_FUTURES_USD_TESTNET_WS_URL, BINANCE_FUTURES_USD_WS_PRIVATE_URL,
26        BINANCE_FUTURES_USD_WS_PUBLIC_URL, BINANCE_FUTURES_USD_WS_URL, BINANCE_OPTIONS_HTTP_URL,
27        BINANCE_OPTIONS_TESTNET_HTTP_URL, BINANCE_OPTIONS_TESTNET_WS_PRIVATE_URL,
28        BINANCE_OPTIONS_TESTNET_WS_PUBLIC_URL, BINANCE_OPTIONS_TESTNET_WS_URL,
29        BINANCE_OPTIONS_WS_URL, BINANCE_SPOT_DEMO_HTTP_URL, BINANCE_SPOT_DEMO_WS_URL,
30        BINANCE_SPOT_HTTP_URL, BINANCE_SPOT_TESTNET_HTTP_URL, BINANCE_SPOT_TESTNET_WS_URL,
31        BINANCE_SPOT_WS_URL, BINANCE_US_SPOT_HTTP_URL, BINANCE_US_SPOT_USER_WS_URL,
32        BINANCE_US_SPOT_WS_URL,
33    },
34    enums::{BinanceEnvironment, BinanceProductType},
35};
36
37/// Returns the HTTP base URL for the given product type and environment.
38#[must_use]
39pub fn get_http_base_url(
40    product_type: BinanceProductType,
41    environment: BinanceEnvironment,
42) -> &'static str {
43    match (product_type, environment) {
44        // Live
45        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Live) => {
46            BINANCE_SPOT_HTTP_URL
47        }
48        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_HTTP_URL,
49        (BinanceProductType::CoinM, BinanceEnvironment::Live) => BINANCE_FUTURES_COIN_HTTP_URL,
50        (BinanceProductType::Options, BinanceEnvironment::Live) => BINANCE_OPTIONS_HTTP_URL,
51
52        // Testnet
53        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
54            BINANCE_SPOT_TESTNET_HTTP_URL
55        }
56        (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
57            BINANCE_FUTURES_USD_TESTNET_HTTP_URL
58        }
59        (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
60            BINANCE_FUTURES_COIN_TESTNET_HTTP_URL
61        }
62        (BinanceProductType::Options, BinanceEnvironment::Testnet) => {
63            BINANCE_OPTIONS_TESTNET_HTTP_URL
64        }
65
66        // Demo
67        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
68            BINANCE_SPOT_DEMO_HTTP_URL
69        }
70        (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_HTTP_URL,
71        (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_HTTP_URL,
72        (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_TESTNET_HTTP_URL,
73    }
74}
75
76/// Returns the HTTP base URL, including first-class Binance US routing.
77#[must_use]
78pub fn get_http_base_url_with_us(
79    product_type: BinanceProductType,
80    environment: BinanceEnvironment,
81    us: bool,
82) -> &'static str {
83    if us {
84        BINANCE_US_SPOT_HTTP_URL
85    } else {
86        get_http_base_url(product_type, environment)
87    }
88}
89
90/// Returns the SAPI base URL for the given environment, or `None` where SAPI is unavailable.
91///
92/// SAPI endpoints (`/sapi/v1/...`) are served from the Spot host on the live exchange. The
93/// testnet and demo hosts do not route `/sapi/v1`, so callers must treat `None` as unavailable
94/// rather than falling back to live, which would route account management against real funds.
95#[must_use]
96pub fn get_sapi_base_url(environment: BinanceEnvironment) -> Option<&'static str> {
97    match environment {
98        BinanceEnvironment::Live => Some(BINANCE_SPOT_HTTP_URL),
99        BinanceEnvironment::Testnet | BinanceEnvironment::Demo => None,
100    }
101}
102
103/// Returns the WebSocket base URL for the given product type and environment.
104#[must_use]
105pub fn get_ws_base_url(
106    product_type: BinanceProductType,
107    environment: BinanceEnvironment,
108) -> &'static str {
109    match (product_type, environment) {
110        // Live
111        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Live) => {
112            BINANCE_SPOT_WS_URL
113        }
114        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_URL,
115        (BinanceProductType::CoinM, BinanceEnvironment::Live) => BINANCE_FUTURES_COIN_WS_URL,
116        (BinanceProductType::Options, BinanceEnvironment::Live) => BINANCE_OPTIONS_WS_URL,
117
118        // Testnet
119        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Testnet) => {
120            BINANCE_SPOT_TESTNET_WS_URL
121        }
122        (BinanceProductType::UsdM, BinanceEnvironment::Testnet) => {
123            BINANCE_FUTURES_USD_TESTNET_WS_URL
124        }
125        (BinanceProductType::CoinM, BinanceEnvironment::Testnet) => {
126            BINANCE_FUTURES_COIN_TESTNET_WS_URL
127        }
128        (BinanceProductType::Options, BinanceEnvironment::Testnet) => {
129            BINANCE_OPTIONS_TESTNET_WS_URL
130        }
131
132        // Demo
133        (BinanceProductType::Spot | BinanceProductType::Margin, BinanceEnvironment::Demo) => {
134            BINANCE_SPOT_DEMO_WS_URL
135        }
136        (BinanceProductType::UsdM, BinanceEnvironment::Demo) => BINANCE_FUTURES_USD_DEMO_WS_URL,
137        (BinanceProductType::CoinM, BinanceEnvironment::Demo) => BINANCE_FUTURES_COIN_DEMO_WS_URL,
138        (BinanceProductType::Options, BinanceEnvironment::Demo) => BINANCE_OPTIONS_TESTNET_WS_URL,
139    }
140}
141
142/// Returns the WebSocket base URL, including first-class Binance US routing.
143#[must_use]
144pub fn get_ws_base_url_with_us(
145    product_type: BinanceProductType,
146    environment: BinanceEnvironment,
147    us: bool,
148) -> &'static str {
149    if us {
150        BINANCE_US_SPOT_WS_URL
151    } else {
152        get_ws_base_url(product_type, environment)
153    }
154}
155
156/// Returns a Spot user stream URL bound to the supplied listen key.
157#[must_use]
158pub(crate) fn get_spot_user_stream_url(base_url: Option<&str>, listen_key: &str) -> String {
159    let base_url = base_url.unwrap_or(BINANCE_US_SPOT_USER_WS_URL);
160    let normalized = base_url.trim_end_matches('/');
161    if normalized.ends_with("/ws") {
162        format!("{normalized}/{listen_key}")
163    } else {
164        format!("{normalized}/ws/{listen_key}")
165    }
166}
167
168/// Returns the WebSocket public stream base URL for high-frequency book data.
169///
170/// USD-M live exchange uses the dedicated public endpoint for `@bookTicker` and
171/// `@depth` streams. All other product types and environments fall back to
172/// [`get_ws_base_url`].
173#[must_use]
174pub fn get_ws_public_base_url(
175    product_type: BinanceProductType,
176    environment: BinanceEnvironment,
177) -> &'static str {
178    match (product_type, environment) {
179        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_PUBLIC_URL,
180        (BinanceProductType::Options, BinanceEnvironment::Testnet | BinanceEnvironment::Demo) => {
181            BINANCE_OPTIONS_TESTNET_WS_PUBLIC_URL
182        }
183        _ => get_ws_base_url(product_type, environment),
184    }
185}
186
187/// Returns the WebSocket private stream base URL for user data.
188///
189/// USD-M live exchange uses the dedicated private endpoint. All other
190/// product types and environments fall back to [`get_ws_base_url`].
191#[must_use]
192pub fn get_ws_private_base_url(
193    product_type: BinanceProductType,
194    environment: BinanceEnvironment,
195) -> &'static str {
196    match (product_type, environment) {
197        (BinanceProductType::UsdM, BinanceEnvironment::Live) => BINANCE_FUTURES_USD_WS_PRIVATE_URL,
198        (BinanceProductType::Options, BinanceEnvironment::Testnet | BinanceEnvironment::Demo) => {
199            BINANCE_OPTIONS_TESTNET_WS_PRIVATE_URL
200        }
201        _ => get_ws_base_url(product_type, environment),
202    }
203}
204
205/// Returns a Futures user stream URL bound to the supplied listen key.
206#[must_use]
207pub(crate) fn get_futures_user_stream_url(
208    product_type: BinanceProductType,
209    base_url: &str,
210    listen_key: &str,
211) -> String {
212    assert!(
213        matches!(
214            product_type,
215            BinanceProductType::UsdM | BinanceProductType::CoinM
216        ),
217        "Futures user stream requires UsdM or CoinM product type, was {product_type:?}"
218    );
219
220    let mut normalized = base_url.trim_end_matches('/').to_string();
221    let path = normalized
222        .split_once("://")
223        .map_or(normalized.as_str(), |(_, rest)| rest)
224        .split_once('/')
225        .map(|(_, path)| path);
226    if matches!(path, None | Some("" | "private")) {
227        normalized.push_str("/ws");
228    }
229
230    match product_type {
231        BinanceProductType::UsdM => format!("{normalized}?listenKey={listen_key}"),
232        BinanceProductType::CoinM => format!("{normalized}/{listen_key}"),
233        _ => unreachable!(),
234    }
235}
236
237fn is_usdm_ws_host(base_url: &str) -> bool {
238    // Strip scheme (e.g. `wss://`) and trailing path/port, then match the hostname.
239    // Accepts fstream.binance.com, fstream-mm.binance.com, fstream-auth.binance.com,
240    // and their .us counterparts, without admitting arbitrary substrings.
241    let without_scheme = base_url
242        .split_once("://")
243        .map_or(base_url, |(_, rest)| rest);
244    let host = without_scheme
245        .split(['/', ':'])
246        .next()
247        .unwrap_or(without_scheme);
248    host.starts_with("fstream") && (host.ends_with(".binance.com") || host.ends_with(".binance.us"))
249}
250
251/// Returns a routed USD-M Futures WebSocket URL derived from an override.
252///
253/// Binance now routes USD-M Futures live traffic by category. This helper
254/// accepts either a root override (for example `wss://fstream.binance.com`) or
255/// a routed/transport-specific override such as `/market`, `/public/ws`, or
256/// `/private/stream`, then rebuilds the URL for the requested route.
257///
258/// URLs that do not point at `fstream.binance.com` (for example local test
259/// endpoints) are returned unchanged.
260#[must_use]
261pub(crate) fn get_usdm_ws_route_base_url(base_url: &str, route: &str) -> String {
262    const SUFFIXES: [&str; 11] = [
263        "/market/ws",
264        "/market/stream",
265        "/public/ws",
266        "/public/stream",
267        "/private/ws",
268        "/private/stream",
269        "/market",
270        "/public",
271        "/private",
272        "/ws",
273        "/stream",
274    ];
275
276    assert!(
277        matches!(route, "market" | "public" | "private"),
278        "invalid USD-M WebSocket route: {route}"
279    );
280
281    if !is_usdm_ws_host(base_url) {
282        return base_url.to_string();
283    }
284
285    let mut normalized = base_url.trim_end_matches('/').to_string();
286
287    for suffix in SUFFIXES {
288        if normalized.ends_with(suffix) {
289            normalized.truncate(normalized.len() - suffix.len());
290            break;
291        }
292    }
293
294    format!("{normalized}/{route}/ws")
295}
296
297#[cfg(test)]
298mod tests {
299    use rstest::rstest;
300
301    use super::*;
302
303    #[rstest]
304    fn test_http_url_spot_live() {
305        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
306        assert_eq!(url, "https://api.binance.com");
307    }
308
309    #[rstest]
310    fn test_binance_us_spot_urls() {
311        let http =
312            get_http_base_url_with_us(BinanceProductType::Spot, BinanceEnvironment::Live, true);
313        let public_ws =
314            get_ws_base_url_with_us(BinanceProductType::Spot, BinanceEnvironment::Live, true);
315        let user_ws = get_spot_user_stream_url(None, "listen-key");
316
317        assert_eq!(http, "https://api.binance.us");
318        assert_eq!(public_ws, "wss://stream.binance.us:9443/ws");
319        assert_eq!(user_ws, "wss://stream.binance.us:443/ws/listen-key");
320    }
321
322    #[rstest]
323    #[case("wss://custom.example/ws", "wss://custom.example/ws/listen-key")]
324    #[case("wss://custom.example", "wss://custom.example/ws/listen-key")]
325    #[case("wss://custom.example/ws/", "wss://custom.example/ws/listen-key")]
326    fn test_spot_user_stream_url_override(#[case] base: &str, #[case] expected: &str) {
327        assert_eq!(get_spot_user_stream_url(Some(base), "listen-key"), expected);
328    }
329
330    #[rstest]
331    fn test_http_url_spot_testnet() {
332        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Testnet);
333        assert_eq!(url, "https://testnet.binance.vision");
334    }
335
336    #[rstest]
337    fn test_http_url_spot_demo() {
338        let url = get_http_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
339        assert_eq!(url, "https://demo-api.binance.com");
340    }
341
342    #[rstest]
343    fn test_http_url_usdm_live() {
344        let url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
345        assert_eq!(url, "https://fapi.binance.com");
346    }
347
348    #[rstest]
349    fn test_urls_usdm_testnet() {
350        let http_url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
351        let ws_url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Testnet);
352
353        assert_eq!(http_url, "https://testnet.binancefuture.com");
354        assert_eq!(ws_url, "wss://fstream.binancefuture.com/ws");
355    }
356
357    #[rstest]
358    fn test_http_url_coinm_live() {
359        let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Live);
360        assert_eq!(url, "https://dapi.binance.com");
361    }
362
363    #[rstest]
364    fn test_urls_usdm_demo() {
365        let http_url = get_http_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
366        let ws_url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Demo);
367
368        assert_eq!(http_url, "https://demo-fapi.binance.com");
369        assert_eq!(ws_url, "wss://demo-fstream.binance.com/ws");
370    }
371
372    #[rstest]
373    fn test_http_url_coinm_demo() {
374        let url = get_http_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
375        assert_eq!(url, "https://demo-dapi.binance.com");
376    }
377
378    #[rstest]
379    fn test_http_url_options_testnet() {
380        let url = get_http_base_url(BinanceProductType::Options, BinanceEnvironment::Testnet);
381        assert_eq!(url, "https://testnet.binancefuture.com");
382    }
383
384    #[rstest]
385    fn test_http_url_options_demo() {
386        let url = get_http_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
387        assert_eq!(url, "https://testnet.binancefuture.com");
388    }
389
390    #[rstest]
391    fn test_ws_url_spot_live() {
392        let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
393        assert_eq!(url, "wss://stream.binance.com:9443/ws");
394    }
395
396    #[rstest]
397    fn test_ws_url_spot_demo() {
398        let url = get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Demo);
399        assert_eq!(url, "wss://demo-stream.binance.com/ws");
400    }
401
402    #[rstest]
403    fn test_ws_url_usdm_live() {
404        let url = get_ws_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
405        assert_eq!(url, "wss://fstream.binance.com/market/ws");
406    }
407
408    #[rstest]
409    fn test_ws_url_coinm_demo() {
410        let url = get_ws_base_url(BinanceProductType::CoinM, BinanceEnvironment::Demo);
411        assert_eq!(url, "wss://demo-dstream.binance.com/ws");
412    }
413
414    #[rstest]
415    fn test_ws_url_options_testnet() {
416        let url = get_ws_base_url(BinanceProductType::Options, BinanceEnvironment::Testnet);
417        assert_eq!(url, "wss://fstream.binancefuture.com/market/ws");
418    }
419
420    #[rstest]
421    fn test_ws_url_options_demo() {
422        let url = get_ws_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
423        assert_eq!(url, "wss://fstream.binancefuture.com/market/ws");
424    }
425
426    #[rstest]
427    fn test_ws_private_url_usdm_live() {
428        let url = get_ws_private_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
429        assert_eq!(url, "wss://fstream.binance.com/private/ws");
430    }
431
432    #[rstest]
433    fn test_ws_private_url_fallback_to_market() {
434        let url = get_ws_private_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
435        assert_eq!(
436            url,
437            get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live)
438        );
439    }
440
441    #[rstest]
442    fn test_ws_public_url_usdm_live() {
443        let url = get_ws_public_base_url(BinanceProductType::UsdM, BinanceEnvironment::Live);
444        assert_eq!(url, "wss://fstream.binance.com/public/ws");
445    }
446
447    #[rstest]
448    fn test_ws_public_url_options_demo() {
449        let url = get_ws_public_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
450        assert_eq!(url, "wss://fstream.binancefuture.com/public/ws");
451    }
452
453    #[rstest]
454    fn test_ws_private_url_options_demo() {
455        let url = get_ws_private_base_url(BinanceProductType::Options, BinanceEnvironment::Demo);
456        assert_eq!(url, "wss://fstream.binancefuture.com/private/ws");
457    }
458
459    #[rstest]
460    #[case(
461        BinanceProductType::UsdM,
462        "wss://fstream.binance.com/private/ws",
463        "wss://fstream.binance.com/private/ws?listenKey=redacted"
464    )]
465    #[case(
466        BinanceProductType::CoinM,
467        "wss://dstream.binance.com/ws",
468        "wss://dstream.binance.com/ws/redacted"
469    )]
470    #[case(
471        BinanceProductType::CoinM,
472        "wss://dstream.binancefuture.com/ws",
473        "wss://dstream.binancefuture.com/ws/redacted"
474    )]
475    #[case(
476        BinanceProductType::CoinM,
477        "wss://demo-dstream.binance.com/ws",
478        "wss://demo-dstream.binance.com/ws/redacted"
479    )]
480    #[case(
481        BinanceProductType::CoinM,
482        "wss://custom.example/ws/",
483        "wss://custom.example/ws/redacted"
484    )]
485    #[case(
486        BinanceProductType::CoinM,
487        "wss://custom.example",
488        "wss://custom.example/ws/redacted"
489    )]
490    #[case(
491        BinanceProductType::UsdM,
492        "ws://127.0.0.1:1234/ws-inject",
493        "ws://127.0.0.1:1234/ws-inject?listenKey=redacted"
494    )]
495    #[case(
496        BinanceProductType::CoinM,
497        "ws://127.0.0.1:1234/ws-inject",
498        "ws://127.0.0.1:1234/ws-inject/redacted"
499    )]
500    fn test_futures_user_stream_url(
501        #[case] product_type: BinanceProductType,
502        #[case] base_url: &str,
503        #[case] expected: &str,
504    ) {
505        let url = get_futures_user_stream_url(product_type, base_url, "redacted");
506        assert_eq!(url, expected);
507    }
508
509    #[rstest]
510    #[should_panic(expected = "Futures user stream requires UsdM or CoinM product type")]
511    fn test_futures_user_stream_url_rejects_non_futures_product() {
512        let _ = get_futures_user_stream_url(
513            BinanceProductType::Spot,
514            "wss://stream.binance.com/ws",
515            "redacted",
516        );
517    }
518
519    #[rstest]
520    fn test_ws_public_url_fallback_to_market() {
521        let url = get_ws_public_base_url(BinanceProductType::Spot, BinanceEnvironment::Live);
522        assert_eq!(
523            url,
524            get_ws_base_url(BinanceProductType::Spot, BinanceEnvironment::Live)
525        );
526    }
527
528    #[rstest]
529    #[case(
530        "wss://fstream.binance.com",
531        "market",
532        "wss://fstream.binance.com/market/ws"
533    )]
534    #[case(
535        "wss://fstream.binance.com/ws",
536        "public",
537        "wss://fstream.binance.com/public/ws"
538    )]
539    #[case(
540        "wss://fstream.binance.com/market/ws",
541        "private",
542        "wss://fstream.binance.com/private/ws"
543    )]
544    #[case(
545        "wss://fstream-mm.binance.com",
546        "market",
547        "wss://fstream-mm.binance.com/market/ws"
548    )]
549    #[case(
550        "wss://fstream-mm.binance.com/ws",
551        "public",
552        "wss://fstream-mm.binance.com/public/ws"
553    )]
554    #[case(
555        "wss://fstream-auth.binance.com/market/ws",
556        "private",
557        "wss://fstream-auth.binance.com/private/ws"
558    )]
559    #[case(
560        "wss://fstream.binance.us",
561        "market",
562        "wss://fstream.binance.us/market/ws"
563    )]
564    fn test_usdm_ws_route_base_url_normalizes_override(
565        #[case] base_url: &str,
566        #[case] route: &str,
567        #[case] expected: &str,
568    ) {
569        let url = get_usdm_ws_route_base_url(base_url, route);
570        assert_eq!(url, expected);
571    }
572
573    #[rstest]
574    #[case("ws://127.0.0.1:9999/ws", "market")]
575    #[case("wss://other.example.com/private/ws", "private")]
576    #[case("ws://localhost:8080", "public")]
577    #[case("wss://other-fstream.binance.com.example.org/ws", "market")]
578    #[case("wss://fstream.binance.com.example.org/ws", "market")]
579    fn test_usdm_ws_route_base_url_passes_through_non_binance_host(
580        #[case] base_url: &str,
581        #[case] route: &str,
582    ) {
583        let url = get_usdm_ws_route_base_url(base_url, route);
584        assert_eq!(url, base_url);
585    }
586
587    #[rstest]
588    #[case(BinanceEnvironment::Live, Some("https://api.binance.com"))]
589    #[case(BinanceEnvironment::Testnet, None)]
590    #[case(BinanceEnvironment::Demo, None)]
591    fn test_sapi_base_url(#[case] environment: BinanceEnvironment, #[case] expected: Option<&str>) {
592        let url = get_sapi_base_url(environment);
593        assert_eq!(url, expected);
594    }
595}