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 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";