Skip to main content

mock_omaha_server/
lib.rs

1// Copyright 2020 The Fuchsia Authors
2//
3// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
6// This file may not be copied, modified, or distributed except according to
7// those terms.
8
9use 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        // This default uses examples from Fuchsia, https://fuchsia.dev/
49        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
62// The corresponding private key to lib/omaha-client's PublicKey. For testing
63// only, since omaha-client never needs to hold a private key.
64pub 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
110/// Trait for spawning background futures in an executor/runtime-agnostic way.
111pub trait Executor {
112    /// Spawns a future on the executor.
113    fn spawn(&self, fut: impl Future<Output = ()> + Send + 'static);
114}
115
116/// An abstract async listener for incoming network connections.
117///
118/// Implementations of this trait allow `OmahaServer` to bind and accept socket
119/// connections without depending on a specific async TCP stream implementation.
120pub trait Listener {
121    /// The async I/O stream type that implements Hyper's Read and Write traits.
122    type Io: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static;
123
124    /// The error type returned when accepting connections.
125    type Error;
126
127    /// Accepts an incoming connection asynchronously.
128    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    /// Sets the special assertion to make on any future update check requests
147    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    /// Sets the special assertion to make on any future cohort in requests
154    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    /// Start the server with a custom listener and executor, running the accept loop.
161    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/// An [`Executor`] implementation that spawns futures on the `tokio` runtime.
183#[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    // If this request contains updatecheck, make sure the mock has the right number of configured apps.
314    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    // It is only possible to calculate an induced etag if the incoming request
503    // had a valid cup2key query argument.
504    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        // Send a request with no specified version and assert that we don't check.
539        // See 0.0.0.1 vs 9.9.9.9 below.
540        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            // change the expected responses; now we only configure one app,
660            // 'integration-test-appid-1', which will respond with an update.
661            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                // We configured 'integration-test-appid-1' to respond with an update.
706                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        // CUP attached.
789        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        // no CUP, but we set .require_cup(true) above, so mock-omaha-server will
844        // panic. (See should_panic above.)
845        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}