Skip to main content

betfair_rpc_server_mock/
lib.rs

1use betfair_adapter::jurisdiction::CustomUrl;
2use betfair_adapter::{
3    ApplicationKey, BetfairConfigBuilder, BetfairRpcClient, BotLogin, Identity, InteractiveLogin,
4    KeepAlive, Logout, Password, RestBase, SecretProvider, Stream, Unauthenticated, Username,
5};
6use betfair_types::types::BetfairRpcRequest;
7use serde_json::json;
8pub use wiremock;
9use wiremock::matchers::{PathExactMatcher, method, path};
10use wiremock::{Mock, MockBuilder, MockServer, ResponseTemplate};
11
12mod urlencoded_matcher;
13use urlencoded_matcher::FormEncodedBodyMatcher;
14
15pub const USERNAME: &str = "usrn";
16pub const PASSWORD: &str = "pasw";
17pub const APP_KEY: &str = "qa{n}pCPTV]EYTLGVO";
18pub const LOGOUT: &str = "/login/";
19pub const LOGIN_URL: &str = "/login/";
20pub const BOT_LOGIN_URL: &str = "/cert-login/";
21pub const KEEP_ALIVE_URL: &str = "/keep-alive/";
22pub const REST_URL: &str = "/rpc/v1/";
23pub const STREAM_URL: &str = "/stream/";
24pub const SESSION_TOKEN: &str = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
25
26#[must_use]
27pub fn rpc_path<T: BetfairRpcRequest>() -> String {
28    format!("{REST_URL}{}", T::method())
29}
30
31pub struct Server {
32    pub bf_api_mock_server: MockServer,
33    pub mock_settings: MockSettings,
34}
35
36#[derive(Debug, Clone)]
37pub struct MockSettings {
38    pub keep_alive_period: core::time::Duration,
39    pub health_check_period: core::time::Duration,
40    pub stream_url: CustomUrl<Stream>,
41}
42
43impl Default for MockSettings {
44    fn default() -> Self {
45        Self {
46            keep_alive_period: core::time::Duration::from_secs(10),
47            health_check_period: core::time::Duration::from_secs(10),
48            stream_url: CustomUrl::new("http://localhost:80/stream".parse().unwrap()),
49        }
50    }
51}
52
53impl Server {
54    pub async fn new() -> Self {
55        Self::new_with_settings(MockSettings::default()).await
56    }
57
58    pub async fn new_with_stream_url(stream_url: CustomUrl<Stream>) -> Self {
59        let settings = MockSettings {
60            stream_url,
61            ..Default::default()
62        };
63        Self::new_with_settings(settings).await
64    }
65
66    pub async fn new_with_settings(mock_settings: MockSettings) -> Self {
67        let mock_server = MockServer::start().await;
68        let login_response = json!(
69            {
70                "sessionToken": SESSION_TOKEN,
71                "loginStatus": "SUCCESS"
72            }
73        );
74        Mock::given(method("POST"))
75            .and(path(BOT_LOGIN_URL))
76            .and(FormEncodedBodyMatcher::new(vec![
77                ("username".to_owned(), USERNAME.to_owned()),
78                ("password".to_owned(), PASSWORD.to_owned()),
79            ]))
80            .respond_with(ResponseTemplate::new(200).set_body_json(login_response))
81            .named("Login")
82            .mount(&mock_server)
83            .await;
84
85        Self {
86            bf_api_mock_server: mock_server,
87            mock_settings,
88        }
89    }
90
91    /// Create a betfair client with the mock server as the base url
92    pub async fn client(&self) -> BetfairRpcClient<Unauthenticated> {
93        let secrets_provider = self.secrets_provider();
94        let config = self.betfair_config(secrets_provider);
95
96        BetfairRpcClient::new_with_config(config).unwrap()
97    }
98
99    #[must_use]
100    pub fn secrets_provider(&self) -> SecretProvider {
101        let identity = reqwest::Identity::from_pem(CERTIFICATE.as_bytes()).unwrap();
102
103        SecretProvider {
104            application_key: ApplicationKey::new(APP_KEY.to_owned()),
105            identity: Identity::new(identity),
106            password: Password::new(PASSWORD.to_owned()),
107            username: Username::new(USERNAME.to_owned()),
108        }
109    }
110
111    #[must_use]
112    #[allow(clippy::type_complexity)]
113    pub fn betfair_config(
114        &self,
115        secrets_provider: SecretProvider,
116    ) -> BetfairConfigBuilder<
117        CustomUrl<RestBase>,
118        CustomUrl<KeepAlive>,
119        CustomUrl<BotLogin>,
120        CustomUrl<Logout>,
121        CustomUrl<InteractiveLogin>,
122        CustomUrl<Stream>,
123    > {
124        let base_uri: url::Url = self.bf_api_mock_server.uri().parse().unwrap();
125
126        BetfairConfigBuilder {
127            rest: CustomUrl::new(base_uri.join(REST_URL).unwrap()),
128            keep_alive: CustomUrl::new(base_uri.join(KEEP_ALIVE_URL).unwrap()),
129            bot_login: CustomUrl::new(base_uri.join(BOT_LOGIN_URL).unwrap()),
130            logout: CustomUrl::new(base_uri.join(LOGOUT).unwrap()),
131            login: CustomUrl::new(base_uri.join(LOGIN_URL).unwrap()),
132            stream: self.mock_settings.stream_url.clone(),
133            secrets_provider,
134        }
135    }
136
137    pub fn mock_success(
138        &self,
139        http_method: &str,
140        path_matcher: PathExactMatcher,
141        name: &str,
142        with_auth_headers: bool,
143        response: serde_json::Value,
144    ) -> Mock {
145        self.mock_low(
146            http_method,
147            path_matcher,
148            name,
149            with_auth_headers,
150            response,
151            200,
152        )
153    }
154
155    pub fn mock_error(
156        &self,
157        http_method: &str,
158        path_matcher: PathExactMatcher,
159        name: &str,
160        with_auth_headers: bool,
161        response: serde_json::Value,
162    ) -> Mock {
163        self.mock_low(
164            http_method,
165            path_matcher,
166            name,
167            with_auth_headers,
168            response,
169            400,
170        )
171    }
172
173    pub fn mock_low(
174        &self,
175        http_method: &str,
176        path_matcher: PathExactMatcher,
177        name: &str,
178        with_auth_headers: bool,
179        response: serde_json::Value,
180        response_code: u16,
181    ) -> Mock {
182        self.mock_builder(http_method, path_matcher, with_auth_headers)
183            .respond_with(ResponseTemplate::new(response_code).set_body_json(response))
184            .named(name)
185    }
186
187    pub fn mock_builder(
188        &self,
189        http_method: &str,
190        path_matcher: PathExactMatcher,
191        with_auth_headers: bool,
192    ) -> MockBuilder {
193        use wiremock::matchers::{header, method};
194
195        let m = Mock::given(method(http_method)).and(path_matcher);
196
197        if with_auth_headers {
198            m.and(header("Accept", "application/json"))
199                .and(header("X-Authentication", SESSION_TOKEN))
200                .and(header("X-Application", APP_KEY))
201        } else {
202            m
203        }
204    }
205
206    pub fn mock_keep_alive(&self) -> Mock {
207        let response = json!(
208            {
209                "token": SESSION_TOKEN,
210                "product":"AppKey",
211                "status": "SUCCESS",
212                "error":""
213            }
214        );
215
216        self.mock_success("GET", path(KEEP_ALIVE_URL), "Keep alive", true, response)
217    }
218
219    pub fn mock_authenticated_rpc<T: BetfairRpcRequest>(&self, response: T::Res) -> Mock
220    where
221        T::Res: serde::Serialize,
222    {
223        self.mock_authenticated_rpc_from_json::<T>(serde_json::to_value(&response).unwrap())
224    }
225
226    pub fn mock_authenticated_error<T: BetfairRpcRequest>(&self, response: T::Error) -> Mock
227    where
228        T::Error: serde::Serialize,
229    {
230        self.mock_error(
231            "POST",
232            path(rpc_path::<T>()),
233            &rpc_path::<T>(),
234            true,
235            serde_json::to_value(response).unwrap(),
236        )
237    }
238
239    pub fn mock_authenticated_rpc_from_json<T: BetfairRpcRequest>(
240        &self,
241        response: serde_json::Value,
242    ) -> Mock {
243        self.mock_success(
244            "POST",
245            path(rpc_path::<T>()),
246            &rpc_path::<T>(),
247            true,
248            serde_json::to_value(response).unwrap(),
249        )
250    }
251}
252
253pub const CERTIFICATE: &str = "-----BEGIN CERTIFICATE-----
254MIIC3zCCAcegAwIBAgIJALAul9kzR0W/MA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNV
255BAYTAmx2MB4XDTIyMDgwMjE5MTE1NloXDTIzMDgwMjE5MTE1NlowDTELMAkGA1UE
256BhMCbHYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8WWPaghYJcXQp
257W/GAoFqKrQIwxy+h8vdZiURVzzqDKt/Mz45x0Zqj8RVSe4S0lLfkRxcgrLz7ZYSc
258TKsVcur8P66F8A2AJaC4KDiYj4azkTtYQDs+RDLRJUCz5xf/Nw7m+6Y0K7p/p2m8
259bPSm6osefz0orQqpwGogqOwI0FKMkU+BpYjMb+k29xbOec6aHxlaPlHLBPa+n3WC
260V96KwmzSMPEN6Fn/G6PZ5PtwmNg769PiXKk02p+hbnx5OCKvi94mn8vVBGgXF6JR
261Vq9IQQvfFm6G6tf7q+yxMdR2FBR2s03t1daJ3RLGdHzXWTAaNRS7E93OWx+ZyTkd
262kIVM16HTAgMBAAGjQjBAMAkGA1UdEwQCMAAwEQYJYIZIAYb4QgEBBAQDAgeAMAsG
263A1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQUFAAOC
264AQEAU/uQHjntyIVR4uQCRoSO5VKyQcXXFY5pbx4ny1yrn0Uxb9P6bOxY5ojcs0r6
265z8ApT3sUfww7kzhle/G5DRtP0cELq7N2YP+qsFx8UO1GYZ5SLj6xm81vk3c0+hrO
266Q3yoS60xKd/7nVsPZ3ch6+9ND0vVUOkefy0aeNix9YgbYjS11rTj7FNiHD25zOJd
267VpZtHkvYDpHcnwUCd0UAuu9ntKKMFGwc9GMqzfY5De6nITvlqzH8YM4AjKO26JsU
2687uMSyHtGF0vvyzhkwCqcuy7r9lQr9m1jTsJ5pSaVasIOJe+/JBUEJm5E4ppdslnW
2691PkfLWOJw34VKkwibWLlwAwTDQ==
270-----END CERTIFICATE-----
271-----BEGIN RSA PRIVATE KEY-----
272MIIEpAIBAAKCAQEAvFlj2oIWCXF0KVvxgKBaiq0CMMcvofL3WYlEVc86gyrfzM+O
273cdGao/EVUnuEtJS35EcXIKy8+2WEnEyrFXLq/D+uhfANgCWguCg4mI+Gs5E7WEA7
274PkQy0SVAs+cX/zcO5vumNCu6f6dpvGz0puqLHn89KK0KqcBqIKjsCNBSjJFPgaWI
275zG/pNvcWznnOmh8ZWj5RywT2vp91glfeisJs0jDxDehZ/xuj2eT7cJjYO+vT4lyp
276NNqfoW58eTgir4veJp/L1QRoFxeiUVavSEEL3xZuhurX+6vssTHUdhQUdrNN7dXW
277id0SxnR811kwGjUUuxPdzlsfmck5HZCFTNeh0wIDAQABAoIBAQCNJFNukCMhanKI
27898xu/js7RlCo6urn6mGvJ+0cfJE1b/CL01HEOzUt+2BmEgetJvDy0M8k/i0UGswY
279MF/YT+iFpNcMqYoEaK4aspFOyedAMuoMxP1gOMz363mkFt3ls4WoVBYFbGtyc6sJ
280t4BSgNpFvUXAcIPYF0ewN8XBCRODH6v7Z6CrbvtjlUXMuU02r5vzMh8a4znIJmZY
28140x6oNIss3YDCGe8J6qMWHByMDZbO63gBoBYayTozzCzl1TG0RZ1oTTL4z36wRto
282uAhjoRek2kiO5axIgKPR/tYlyKzwLkS5v1W09K+pvsabAU6gQlC8kUPk7/+GOaeI
283wGMI9FAZAoGBAOJN8mqJ3zHKvkyFW0uFMU14dl8SVrCZF1VztIooVgnM6bSqNZ3Y
284nKE7wk1DuFjqKAi/mgXTr1v8mQtr40t5dBEMdgDpfRf/RrMfQyhEgQ/m1WqBQtPx
285Suz+EYMpcH05ynrfSbxCDNYM4OHNJ1QfIvHJ/Q9wt5hT7w+MOH5h5TctAoGBANUQ
286cXF4QKU6P+dLUYNjrYP5Wjg4194i0fh/I9NVoUE9Xl22J8l0lybV2phkuODMp1I+
287rBi9AON9skjdCnwtH2ZbRCP6a8Zjv7NMLy4b4dQqfoHwTdCJ0FBfgZXhH4i+AXMb
288XsKotxKGqCWgFKY8LB3UJ0qakK6h9Ze+/zbnZ9z/AoGBAJwrQkD3SAkqakyQMsJY
2899f8KRFWzaBOSciHMKSi2UTmOKTE9zKZTFzPE838yXoMtg9cVsgqXXIpUNKFHIKGy
290/L/PI5fZiTQIPBfcWRHuxEne+CP5c86i0xvc8OTcsf4Y5XwJnu7FfeoxFPd+Bcft
291fMXyqCoBlREPywelsk606+M5AoGAfXLICJJQJbitRYbQQLcgw/K+DxpQ54bC8DgT
292pOvnHR2AAVcuB+xwzrndkhrDzABTiBZEh/BIpKkunr4e3UxID6Eu9qwMZuv2RCBY
293KyLZjW1TvTf66Q0rrRb+mnvJcF7HRbnYym5CFFNaj4S4g8QsCYgPdlqZU2kizCz1
2944aLQQYsCgYAGKytrtHi2BM4Cnnq8Lwd8wT8/1AASIwg2Va1Gcfp00lamuy14O7uz
295yvdFIFrv4ZPdRkf174B1G+FDkH8o3NZ1cf+OuVIKC+jONciIJsYLPTHR0pgWqE4q
296FAbbOyAg51Xklqm2Q954WWFmu3lluHCWUGB9eSHshIurTmDd+8o15A==
297-----END RSA PRIVATE KEY-----
298";