1use anyhow::Error;
10use derive_builder::Builder;
11use hyper::service::service_fn;
12use hyper::{Method, Request, Response, StatusCode, header};
13use omaha_client::cup_ecdsa::PublicKeyId;
14use omaha_client::cup_ecdsa::test_support::{
15 make_default_private_key_for_test, make_default_public_key_id_for_test,
16};
17use omaha_client::http_request::{Body, empty_body, to_bytes};
18use p256::ecdsa::signature::Signer;
19use serde::Deserialize;
20use serde_json::json;
21use sha2::{Digest, Sha256};
22use std::collections::HashMap;
23use std::future::Future;
24use std::sync::{Arc, Mutex};
25use url::Url;
26
27#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
28pub enum OmahaResponse {
29 NoUpdate,
30 Update,
31 UrgentUpdate,
32 InvalidResponse,
33 InvalidURL,
34}
35
36#[derive(Clone, Debug, Deserialize)]
37pub struct ResponseAndMetadata {
38 pub response: OmahaResponse,
39 pub check_assertion: UpdateCheckAssertion,
40 pub version: Option<String>,
41 pub cohort_assertion: Option<String>,
42 pub codebase: String,
43 pub package_name: String,
44}
45
46impl Default for ResponseAndMetadata {
47 fn default() -> ResponseAndMetadata {
48 ResponseAndMetadata {
50 response: OmahaResponse::NoUpdate,
51 check_assertion: UpdateCheckAssertion::UpdatesEnabled,
52 version: Some("0.1.2.3".to_string()),
53 cohort_assertion: None,
54 codebase: "fuchsia-pkg://integration.test.fuchsia.com/".to_string(),
55 package_name:
56 "update?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
57 .to_string(),
58 }
59 }
60}
61
62pub type PrivateKey = p256::ecdsa::SigningKey;
65
66#[derive(Clone, Debug)]
67pub struct PrivateKeyAndId {
68 pub id: PublicKeyId,
69 pub key: PrivateKey,
70}
71
72#[derive(Clone, Debug)]
73pub struct PrivateKeys {
74 pub latest: PrivateKeyAndId,
75 pub historical: Vec<PrivateKeyAndId>,
76}
77
78impl PrivateKeys {
79 pub fn find(&self, id: PublicKeyId) -> Option<&PrivateKey> {
80 if self.latest.id == id {
81 return Some(&self.latest.key);
82 }
83 for pair in &self.historical {
84 if pair.id == id {
85 return Some(&pair.key);
86 }
87 }
88 None
89 }
90}
91
92pub fn make_default_private_keys_for_test() -> PrivateKeys {
93 PrivateKeys {
94 latest: PrivateKeyAndId {
95 id: make_default_public_key_id_for_test(),
96 key: make_default_private_key_for_test(),
97 },
98 historical: vec![],
99 }
100}
101
102pub type ResponseMap = HashMap<String, ResponseAndMetadata>;
103
104#[derive(Copy, Clone, Debug, Deserialize)]
105pub enum UpdateCheckAssertion {
106 UpdatesEnabled,
107 UpdatesDisabled,
108}
109
110pub trait Executor {
112 fn spawn(&self, fut: impl Future<Output = ()> + Send + 'static);
114}
115
116pub trait Listener {
121 type Io: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static;
123
124 type Error;
126
127 fn accept(&mut self) -> impl Future<Output = Result<Self::Io, Self::Error>> + Send;
129}
130
131#[derive(Clone, Debug, Builder)]
132#[builder(pattern = "owned")]
133#[builder(derive(Debug))]
134pub struct OmahaServer {
135 #[builder(default, setter(into))]
136 pub responses_by_appid: ResponseMap,
137 #[builder(default = "make_default_private_keys_for_test()")]
138 pub private_keys: PrivateKeys,
139 #[builder(default = "None")]
140 pub etag_override: Option<String>,
141 #[builder(default)]
142 pub require_cup: bool,
143}
144
145impl OmahaServer {
146 pub fn set_all_update_check_assertions(&mut self, value: UpdateCheckAssertion) {
148 for response_and_metadata in self.responses_by_appid.values_mut() {
149 response_and_metadata.check_assertion = value;
150 }
151 }
152
153 pub fn set_all_cohort_assertions(&mut self, value: Option<String>) {
155 for response_and_metadata in self.responses_by_appid.values_mut() {
156 response_and_metadata.cohort_assertion = value.clone();
157 }
158 }
159
160 pub async fn start(
162 arc_server: Arc<Mutex<OmahaServer>>,
163 mut listener: impl Listener + Send + 'static,
164 executor: impl Executor + Send + 'static,
165 ) -> Result<(), Error> {
166 while let Ok(io) = listener.accept().await {
167 let arc_server = Arc::clone(&arc_server);
168 let service = service_fn(move |req| {
169 let arc_server = Arc::clone(&arc_server);
170 async move { handle_request(req, &arc_server).await }
171 });
172 executor.spawn(async move {
173 let _ =
174 hyper::server::conn::http1::Builder::new().serve_connection(io, service).await;
175 });
176 }
177
178 Ok(())
179 }
180}
181
182#[cfg(feature = "tokio")]
184#[derive(Clone, Copy, Debug, Default)]
185pub struct TokioExecutor;
186
187#[cfg(feature = "tokio")]
188impl Executor for TokioExecutor {
189 fn spawn(&self, fut: impl Future<Output = ()> + Send + 'static) {
190 tokio::spawn(fut);
191 }
192}
193
194#[cfg(feature = "tokio")]
195impl Listener for tokio::net::TcpListener {
196 type Io = hyper_util::rt::TokioIo<tokio::net::TcpStream>;
197 type Error = std::io::Error;
198
199 async fn accept(&mut self) -> Result<Self::Io, Self::Error> {
200 let (stream, _) = tokio::net::TcpListener::accept(self).await?;
201 Ok(hyper_util::rt::TokioIo::new(stream))
202 }
203}
204
205fn make_etag(
206 request_body: &[u8],
207 uri: &str,
208 private_keys: &PrivateKeys,
209 response_data: &[u8],
210) -> Option<String> {
211 if uri == "/" {
212 return None;
213 }
214
215 let parsed_uri = Url::parse(&format!("https://example.com{uri}")).unwrap();
216 let mut query_pairs = parsed_uri.query_pairs();
217
218 let (cup2key_key, cup2key_val) = query_pairs.next().unwrap();
219 assert_eq!(cup2key_key, "cup2key");
220
221 let (public_key_id_str, _nonce_str) = cup2key_val.split_once(':').unwrap();
222 let public_key_id: PublicKeyId = public_key_id_str.parse().unwrap();
223 let private_key: &PrivateKey = match private_keys.find(public_key_id) {
224 Some(pk) => Some(pk),
225 None => {
226 log::error!(
227 "Could not find public_key_id {:?} in the private_keys map, which only knows about the latest key_id {:?} and the historical key_ids {:?}",
228 public_key_id,
229 private_keys.latest.id,
230 private_keys.historical.iter().map(|pkid| pkid.id).collect::<Vec<_>>(),
231 );
232 None
233 }
234 }?;
235
236 let request_hash = Sha256::digest(request_body);
237 let response_hash = Sha256::digest(response_data);
238
239 let mut hasher = Sha256::new();
240 hasher.update(request_hash);
241 hasher.update(response_hash);
242 hasher.update(&*cup2key_val);
243 let transaction_hash = hasher.finalize();
244
245 let sig: p256::ecdsa::Signature = private_key.sign(&transaction_hash);
246 Some(format!("{}:{}", hex::encode(sig.to_der()), hex::encode(request_hash)))
247}
248
249pub async fn handle_request<B>(
250 req: Request<B>,
251 omaha_server: &Mutex<OmahaServer>,
252) -> Result<Response<Body>, Error>
253where
254 B: http_body::Body + std::fmt::Debug + 'static,
255 B::Error: std::error::Error + Send + Sync + 'static,
256{
257 log::debug!("{req:#?}");
258 if req.uri().path() == "/set_responses_by_appid" {
259 return handle_set_responses(req, omaha_server).await;
260 }
261
262 handle_omaha_request(req, omaha_server).await
263}
264
265pub async fn handle_set_responses<B>(
266 req: Request<B>,
267 omaha_server: &Mutex<OmahaServer>,
268) -> Result<Response<Body>, Error>
269where
270 B: http_body::Body + 'static,
271 B::Error: std::error::Error + Send + Sync + 'static,
272{
273 assert_eq!(req.method(), Method::POST);
274
275 let req_body = to_bytes(req).await.map_err(|_| anyhow::anyhow!("failed to read body"))?;
276 let req_json: HashMap<String, ResponseAndMetadata> =
277 serde_json::from_slice(&req_body).expect("parse json");
278 omaha_server.lock().unwrap().responses_by_appid = req_json;
279
280 let builder = Response::builder().status(StatusCode::OK).header(header::CONTENT_LENGTH, 0);
281 Ok(builder.body(empty_body()).unwrap())
282}
283
284pub async fn handle_omaha_request<B>(
285 req: Request<B>,
286 omaha_server: &Mutex<OmahaServer>,
287) -> Result<Response<Body>, Error>
288where
289 B: http_body::Body + 'static,
290 B::Error: std::error::Error + Send + Sync + 'static,
291{
292 let omaha_server = omaha_server.lock().unwrap().clone();
293 assert_eq!(req.method(), Method::POST);
294
295 if omaha_server.responses_by_appid.is_empty() {
296 let builder = Response::builder()
297 .status(StatusCode::INTERNAL_SERVER_ERROR)
298 .header(header::CONTENT_LENGTH, 0);
299 log::error!(
300 "Received a request before |responses_by_appid| was set; returning an empty response with status 500."
301 );
302 return Ok(builder.body(empty_body()).unwrap());
303 }
304
305 let uri_string = req.uri().to_string();
306
307 let req_body = to_bytes(req).await.map_err(|_| anyhow::anyhow!("failed to read body"))?;
308 let req_json: serde_json::Value = serde_json::from_slice(&req_body).expect("parse json");
309
310 let request = req_json.get("request").unwrap();
311 let apps = request.get("app").unwrap().as_array().unwrap();
312
313 match apps.iter().filter(|app| app.get("updatecheck").is_some()).count() {
315 0 => {}
316 x => assert_eq!(x, omaha_server.responses_by_appid.len()),
317 }
318
319 let apps: Vec<serde_json::Value> = apps
320 .iter()
321 .map(|app| {
322 let appid = app.get("appid").unwrap();
323 let expected = &omaha_server.responses_by_appid[appid.as_str().unwrap()];
324
325 if let Some(expected_version) = &expected.version {
326 let version = app.get("version").unwrap();
327 assert_eq!(version, expected_version);
328 }
329
330 if let Some(expected_update_check) = app.get("updatecheck") {
331 let updatedisabled = expected_update_check
332 .get("updatedisabled")
333 .map(|v| v.as_bool().unwrap())
334 .unwrap_or(false);
335 match expected.check_assertion {
336 UpdateCheckAssertion::UpdatesEnabled => {
337 assert!(!updatedisabled);
338 }
339 UpdateCheckAssertion::UpdatesDisabled => {
340 assert!(updatedisabled);
341 }
342 }
343
344 if let Some(cohort_assertion) = &expected.cohort_assertion {
345 assert_eq!(
346 app.get("cohort")
347 .expect("expected cohort")
348 .as_str()
349 .expect("cohort is string"),
350 cohort_assertion
351 );
352 }
353
354 let updatecheck = match expected.response {
355 OmahaResponse::Update => json!({
356 "status": "ok",
357 "urls": {
358 "url": [
359 {
360 "codebase": expected.codebase,
361 }
362 ]
363 },
364 "manifest": {
365 "version": "0.1.2.3",
366 "actions": {
367 "action": [
368 {
369 "run": &expected.package_name,
370 "event": "install"
371 },
372 {
373 "event": "postinstall"
374 }
375 ]
376 },
377 "packages": {
378 "package": [
379 {
380 "name": &expected.package_name,
381 "fp": "2.0.1.2.3",
382 "required": true
383 }
384 ]
385 }
386 }
387 }),
388 OmahaResponse::UrgentUpdate => json!({
389 "status": "ok",
390 "urls": {
391 "url": [
392 {
393 "codebase": expected.codebase,
394 }
395 ]
396 },
397 "manifest": {
398 "version": "0.1.2.3",
399 "actions": {
400 "action": [
401 {
402 "run": &expected.package_name,
403 "event": "install"
404 },
405 {
406 "event": "postinstall"
407 }
408 ]
409 },
410 "packages": {
411 "package": [
412 {
413 "name": &expected.package_name,
414 "fp": "2.0.1.2.3",
415 "required": true
416 }
417 ]
418 }
419 },
420 "_urgent_update": true
421 }),
422 OmahaResponse::NoUpdate => json!({
423 "status": "noupdate",
424 }),
425 OmahaResponse::InvalidResponse => json!({
426 "invalid_status": "invalid",
427 }),
428 OmahaResponse::InvalidURL => json!({
429 "status": "ok",
430 "urls": {
431 "url": [
432 {
433 "codebase": "http://integration.test.fuchsia.com/"
434 }
435 ]
436 },
437 "manifest": {
438 "version": "0.1.2.3",
439 "actions": {
440 "action": [
441 {
442 "run": &expected.package_name,
443 "event": "install"
444 },
445 {
446 "event": "postinstall"
447 }
448 ]
449 },
450 "packages": {
451 "package": [
452 {
453 "name": &expected.package_name,
454 "fp": "2.0.1.2.3",
455 "required": true
456 }
457 ]
458 }
459 }
460 }),
461 };
462 json!(
463 {
464 "cohorthint": "integration-test",
465 "appid": appid,
466 "cohort": "1:1:",
467 "status": "ok",
468 "cohortname": "integration-test",
469 "updatecheck": updatecheck,
470 })
471 } else {
472 assert!(app.get("event").is_some());
473 json!(
474 {
475 "cohorthint": "integration-test",
476 "appid": appid,
477 "cohort": "1:1:",
478 "status": "ok",
479 "cohortname": "integration-test",
480 })
481 }
482 })
483 .collect();
484 let response = json!({
485 "response": {
486 "server": "prod",
487 "protocol": "3.0",
488 "daystart": {
489 "elapsed_seconds": 48810,
490 "elapsed_days": 4775
491 },
492 "app": apps
493 }
494 });
495
496 let response_data: Vec<u8> = serde_json::to_vec(&response).unwrap();
497
498 let mut builder = Response::builder()
499 .status(StatusCode::OK)
500 .header(header::CONTENT_LENGTH, response_data.len());
501
502 let induced_etag: Option<String> =
505 make_etag(&req_body, &uri_string, &omaha_server.private_keys, &response_data);
506
507 if omaha_server.require_cup && induced_etag.is_none() {
508 panic!(
509 "mock-omaha-server was configured to expect CUP, but we received a request without it."
510 );
511 }
512
513 if let Some(etag) = omaha_server.etag_override.as_ref().or(induced_etag.as_ref()) {
514 builder = builder.header(header::ETAG, etag);
515 }
516
517 Ok(builder.body(Body::from(response_data)).unwrap())
518}
519
520#[cfg(any(all(test, feature = "tokio"), export_testing_macro))]
521pub mod tests {
522 use super::*;
523 use anyhow::Context as _;
524 use hyper_util::client::legacy::connect::Connect;
525 use std::net::{Ipv4Addr, SocketAddr};
526
527 pub async fn test_no_validate_version<S, Fut, C, CFut, Conn>(
528 start_server: S,
529 new_http_client: C,
530 ) -> Result<(), Error>
531 where
532 S: FnOnce(Arc<Mutex<OmahaServer>>) -> Fut,
533 Fut: Future<Output = Result<String, Error>>,
534 C: Fn() -> CFut,
535 CFut: Future<Output = hyper_util::client::legacy::Client<Conn, Body>>,
536 Conn: Connect + Clone + Send + Sync + 'static,
537 {
538 let server = start_server(Arc::new(Mutex::new(
541 OmahaServerBuilder::default()
542 .responses_by_appid([(
543 "integration-test-appid-1".to_string(),
544 ResponseAndMetadata {
545 response: OmahaResponse::NoUpdate,
546 version: None,
547 ..Default::default()
548 },
549 )])
550 .build()
551 .unwrap(),
552 )))
553 .await
554 .context("starting server")?;
555
556 let client = new_http_client().await;
557 let body = json!({
558 "request": {
559 "app": [
560 {
561 "appid": "integration-test-appid-1",
562 "version": "9.9.9.9",
563 "updatecheck": { "updatedisabled": false }
564 },
565 ]
566 }
567 });
568 let request = Request::post(&server).body(Body::from(body.to_string())).unwrap();
569
570 let response = client.request(request).await?;
571
572 assert_eq!(response.status(), StatusCode::OK);
573 let body = to_bytes(response).await.context("reading response body")?;
574 let obj: serde_json::Value =
575 serde_json::from_slice(&body).context("parsing response json")?;
576
577 let response = obj.get("response").unwrap();
578 let apps = response.get("app").unwrap().as_array().unwrap();
579 assert_eq!(apps.len(), 1);
580 let status = apps[0].get("updatecheck").unwrap().get("status").unwrap();
581 assert_eq!(status, "noupdate");
582 Ok(())
583 }
584
585 pub async fn test_server_replies<S, Fut, C, CFut, Conn>(
586 start_server: S,
587 new_http_client: C,
588 ) -> Result<(), Error>
589 where
590 S: FnOnce(Arc<Mutex<OmahaServer>>) -> Fut,
591 Fut: Future<Output = Result<String, Error>>,
592 C: Fn() -> CFut,
593 CFut: Future<Output = hyper_util::client::legacy::Client<Conn, Body>>,
594 Conn: Connect + Clone + Send + Sync + 'static,
595 {
596 let server_url = start_server(Arc::new(Mutex::new(
597 OmahaServerBuilder::default()
598 .responses_by_appid([
599 (
600 "integration-test-appid-1".to_string(),
601 ResponseAndMetadata {
602 response: OmahaResponse::NoUpdate,
603 version: Some("0.0.0.1".to_string()),
604 ..Default::default()
605 },
606 ),
607 (
608 "integration-test-appid-2".to_string(),
609 ResponseAndMetadata {
610 response: OmahaResponse::NoUpdate,
611 version: Some("0.0.0.2".to_string()),
612 ..Default::default()
613 },
614 ),
615 ])
616 .build()
617 .unwrap(),
618 )))
619 .await
620 .context("starting server")?;
621
622 {
623 let client = new_http_client().await;
624 let body = json!({
625 "request": {
626 "app": [
627 {
628 "appid": "integration-test-appid-1",
629 "version": "0.0.0.1",
630 "updatecheck": { "updatedisabled": false }
631 },
632 {
633 "appid": "integration-test-appid-2",
634 "version": "0.0.0.2",
635 "updatecheck": { "updatedisabled": false }
636 },
637 ]
638 }
639 });
640 let request = Request::post(&server_url).body(Body::from(body.to_string())).unwrap();
641
642 let response = client.request(request).await?;
643
644 assert_eq!(response.status(), StatusCode::OK);
645 let body = to_bytes(response).await.context("reading response body")?;
646 let obj: serde_json::Value =
647 serde_json::from_slice(&body).context("parsing response json")?;
648
649 let response = obj.get("response").unwrap();
650 let apps = response.get("app").unwrap().as_array().unwrap();
651 assert_eq!(apps.len(), 2);
652 for app in apps {
653 let status = app.get("updatecheck").unwrap().get("status").unwrap();
654 assert_eq!(status, "noupdate");
655 }
656 }
657
658 {
659 let body = json!({
662 "integration-test-appid-1": {
663 "response": "Update",
664 "check_assertion": "UpdatesEnabled",
665 "version": "0.0.0.1",
666 "codebase": "fuchsia-pkg://integration.test.fuchsia.com/",
667 "package_name": "update?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
668 }
669 });
670 let request = Request::post(format!("{server_url}set_responses_by_appid"))
671 .body(Body::from(body.to_string()))
672 .unwrap();
673 let client = new_http_client().await;
674 let response = client.request(request).await?;
675 assert_eq!(response.status(), StatusCode::OK);
676 }
677
678 {
679 let body = json!({
680 "request": {
681 "app": [
682 {
683 "appid": "integration-test-appid-1",
684 "version": "0.0.0.1",
685 "updatecheck": { "updatedisabled": false }
686 },
687 ]
688 }
689 });
690 let request = Request::post(&server_url).body(Body::from(body.to_string())).unwrap();
691
692 let client = new_http_client().await;
693 let response = client.request(request).await?;
694
695 assert_eq!(response.status(), StatusCode::OK);
696 let body = to_bytes(response).await.context("reading response body")?;
697 let obj: serde_json::Value =
698 serde_json::from_slice(&body).context("parsing response json")?;
699
700 let response = obj.get("response").unwrap();
701 let apps = response.get("app").unwrap().as_array().unwrap();
702 assert_eq!(apps.len(), 1);
703 for app in apps {
704 let status = app.get("updatecheck").unwrap().get("status").unwrap();
705 assert_eq!(status, "ok");
707 }
708 }
709
710 Ok(())
711 }
712
713 pub async fn test_no_configured_responses<S, Fut, C, CFut, Conn>(
714 start_server: S,
715 new_http_client: C,
716 ) -> Result<(), Error>
717 where
718 S: FnOnce(Arc<Mutex<OmahaServer>>) -> Fut,
719 Fut: Future<Output = Result<String, Error>>,
720 C: Fn() -> CFut,
721 CFut: Future<Output = hyper_util::client::legacy::Client<Conn, Body>>,
722 Conn: Connect + Clone + Send + Sync + 'static,
723 {
724 let server = start_server(Arc::new(Mutex::new(
725 OmahaServerBuilder::default().responses_by_appid([]).build().unwrap(),
726 )))
727 .await
728 .context("starting server")?;
729
730 let client = new_http_client().await;
731 let body = json!({
732 "request": {
733 "app": [
734 {
735 "appid": "integration-test-appid-1",
736 "version": "0.1.2.3",
737 "updatecheck": { "updatedisabled": false }
738 },
739 ]
740 }
741 });
742 let request = Request::post(&server).body(Body::from(body.to_string())).unwrap();
743 let response = client.request(request).await?;
744 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
745 Ok(())
746 }
747
748 pub async fn test_server_expect_cup_nopanic<S, Fut, C, CFut, Conn>(
749 start_server: S,
750 new_http_client: C,
751 ) -> Result<(), Error>
752 where
753 S: FnOnce(Arc<Mutex<OmahaServer>>) -> Fut,
754 Fut: Future<Output = Result<String, Error>>,
755 C: Fn() -> CFut,
756 CFut: Future<Output = hyper_util::client::legacy::Client<Conn, Body>>,
757 Conn: Connect + Clone + Send + Sync + 'static,
758 {
759 let server_url = start_server(Arc::new(Mutex::new(
760 OmahaServerBuilder::default()
761 .responses_by_appid([(
762 "integration-test-appid-1".to_string(),
763 ResponseAndMetadata {
764 response: OmahaResponse::NoUpdate,
765 version: Some("0.0.0.1".to_string()),
766 ..Default::default()
767 },
768 )])
769 .require_cup(true)
770 .build()
771 .unwrap(),
772 )))
773 .await
774 .context("starting server")?;
775
776 let client = new_http_client().await;
777 let body = json!({
778 "request": {
779 "app": [
780 {
781 "appid": "integration-test-appid-1",
782 "version": "0.0.0.1",
783 "updatecheck": { "updatedisabled": false }
784 },
785 ]
786 }
787 });
788 let request = Request::post(format!(
790 "{}?cup2key={}:nonce",
791 server_url,
792 make_default_public_key_id_for_test()
793 ))
794 .body(Body::from(body.to_string()))
795 .unwrap();
796
797 let response = client.request(request).await?;
798
799 assert_eq!(response.status(), StatusCode::OK);
800 Ok(())
801 }
802
803 pub async fn test_server_expect_cup_panic<S, Fut, C, CFut, Conn>(
804 start_server: S,
805 new_http_client: C,
806 ) where
807 S: FnOnce(Arc<Mutex<OmahaServer>>) -> Fut,
808 Fut: Future<Output = Result<String, Error>>,
809 C: Fn() -> CFut,
810 CFut: Future<Output = hyper_util::client::legacy::Client<Conn, Body>>,
811 Conn: Connect + Clone + Send + Sync + 'static,
812 {
813 let server_url = start_server(Arc::new(Mutex::new(
814 OmahaServerBuilder::default()
815 .responses_by_appid([(
816 "integration-test-appid-1".to_string(),
817 ResponseAndMetadata {
818 response: OmahaResponse::NoUpdate,
819 version: Some("0.0.0.1".to_string()),
820 ..Default::default()
821 },
822 )])
823 .require_cup(true)
824 .build()
825 .unwrap(),
826 )))
827 .await
828 .context("starting server")
829 .unwrap();
830
831 let client = new_http_client().await;
832 let body = json!({
833 "request": {
834 "app": [
835 {
836 "appid": "integration-test-appid-1",
837 "version": "0.0.0.1",
838 "updatecheck": { "updatedisabled": false }
839 },
840 ]
841 }
842 });
843 let request = Request::post(&server_url).body(Body::from(body.to_string())).unwrap();
846 let _response = client.request(request).await.unwrap();
847 }
848
849 #[macro_export]
850 macro_rules! declare_tests {
851 (
852 test_attr: #[$test_attr:meta],
853 start_server: $start_server:expr,
854 new_http_client: $new_http_client:expr,
855 cup_expect_panic: $panic_expected:expr,
856 ) => {
857 #[$test_attr]
858 async fn test_tokio_no_validate_version() -> Result<(), ::anyhow::Error> {
859 $crate::tests::test_no_validate_version($start_server, $new_http_client).await
860 }
861
862 #[$test_attr]
863 async fn test_tokio_server_replies() -> Result<(), ::anyhow::Error> {
864 $crate::tests::test_server_replies($start_server, $new_http_client).await
865 }
866
867 #[$test_attr]
868 async fn test_tokio_no_configured_responses() -> Result<(), ::anyhow::Error> {
869 $crate::tests::test_no_configured_responses($start_server, $new_http_client).await
870 }
871
872 #[$test_attr]
873 async fn test_tokio_server_expect_cup_nopanic() -> Result<(), ::anyhow::Error> {
874 $crate::tests::test_server_expect_cup_nopanic($start_server, $new_http_client).await
875 }
876
877 #[$test_attr]
878 #[should_panic(expected = $panic_expected)]
879 async fn test_tokio_server_expect_cup_panic() {
880 $crate::tests::test_server_expect_cup_panic($start_server, $new_http_client).await;
881 }
882 };
883 }
884
885 #[cfg(feature = "tokio")]
886 declare_tests! {
887 test_attr: #[tokio::test],
888 start_server: async |server| {
889 let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0);
890 let listener = tokio::net::TcpListener::bind(&addr).await?;
891 let addr = listener.local_addr()?;
892 TokioExecutor.spawn(async move {
893 let _ = OmahaServer::start(server, listener, TokioExecutor).await;
894 });
895 Ok(format!("http://{addr}/"))
896 },
897 new_http_client: async || {
898 hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
899 .build_http()
900 },
901 cup_expect_panic: "hyper::Error(IncompleteMessage)",
902 }
903}