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 futures::prelude::*;
12use hyper::body::Bytes;
13use hyper::server::accept::from_stream;
14use hyper::server::Server;
15use hyper::service::{make_service_fn, service_fn};
16use hyper::{header, Body, Method, Request, Response, StatusCode};
17use omaha_client::cup_ecdsa::test_support::{
18    make_default_private_key_for_test, make_default_public_key_id_for_test,
19};
20use omaha_client::cup_ecdsa::PublicKeyId;
21use serde::Deserialize;
22use serde_json::json;
23use sha2::{Digest, Sha256};
24use std::collections::HashMap;
25use std::convert::Infallible;
26use std::net::{Ipv4Addr, SocketAddr};
27use std::sync::Arc;
28#[cfg(not(target_os = "fuchsia"))]
29use tokio::net::{TcpListener, TcpStream};
30use url::Url;
31
32#[cfg(not(target_os = "fuchsia"))]
33use {
34    std::io,
35    std::pin::Pin,
36    std::task::{Context, Poll},
37    tokio::task::JoinHandle,
38};
39
40#[cfg(all(not(fasync), not(target_os = "fuchsia")))]
41use tokio::sync::Mutex;
42
43#[cfg(fasync)]
44use {fuchsia_async as fasync, fuchsia_async::Task, fuchsia_sync::Mutex};
45
46#[cfg(all(fasync, target_os = "fuchsia"))]
47use fuchsia_async::net::TcpListener;
48
49#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
50pub enum OmahaResponse {
51    NoUpdate,
52    Update,
53    UrgentUpdate,
54    InvalidResponse,
55    InvalidURL,
56}
57
58#[derive(Clone, Debug, Deserialize)]
59pub struct ResponseAndMetadata {
60    pub response: OmahaResponse,
61    pub check_assertion: UpdateCheckAssertion,
62    pub version: Option<String>,
63    pub cohort_assertion: Option<String>,
64    pub codebase: String,
65    pub package_name: String,
66}
67
68impl Default for ResponseAndMetadata {
69    fn default() -> ResponseAndMetadata {
70        // This default uses examples from Fuchsia, https://fuchsia.dev/
71        ResponseAndMetadata {
72            response: OmahaResponse::NoUpdate,
73            check_assertion: UpdateCheckAssertion::UpdatesEnabled,
74            version: Some("0.1.2.3".to_string()),
75            cohort_assertion: None,
76            codebase: "fuchsia-pkg://integration.test.fuchsia.com/".to_string(),
77            package_name:
78                "update?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
79                    .to_string(),
80        }
81    }
82}
83
84// The corresponding private key to lib/omaha-client's PublicKey. For testing
85// only, since omaha-client never needs to hold a private key.
86pub type PrivateKey = p256::ecdsa::SigningKey;
87
88#[derive(Clone, Debug)]
89pub struct PrivateKeyAndId {
90    pub id: PublicKeyId,
91    pub key: PrivateKey,
92}
93
94#[derive(Clone, Debug)]
95pub struct PrivateKeys {
96    pub latest: PrivateKeyAndId,
97    pub historical: Vec<PrivateKeyAndId>,
98}
99
100impl PrivateKeys {
101    pub fn find(&self, id: PublicKeyId) -> Option<&PrivateKey> {
102        if self.latest.id == id {
103            return Some(&self.latest.key);
104        }
105        for pair in &self.historical {
106            if pair.id == id {
107                return Some(&pair.key);
108            }
109        }
110        None
111    }
112}
113
114pub fn make_default_private_keys_for_test() -> PrivateKeys {
115    PrivateKeys {
116        latest: PrivateKeyAndId {
117            id: make_default_public_key_id_for_test(),
118            key: make_default_private_key_for_test(),
119        },
120        historical: vec![],
121    }
122}
123
124pub type ResponseMap = HashMap<String, ResponseAndMetadata>;
125
126#[derive(Copy, Clone, Debug, Deserialize)]
127pub enum UpdateCheckAssertion {
128    UpdatesEnabled,
129    UpdatesDisabled,
130}
131
132/// Adapt [tokio::net::TcpStream] to work with hyper.
133#[cfg(not(target_os = "fuchsia"))]
134#[derive(Debug)]
135pub enum ConnectionStream {
136    Tcp(TcpStream),
137}
138
139#[cfg(not(target_os = "fuchsia"))]
140impl tokio::io::AsyncRead for ConnectionStream {
141    fn poll_read(
142        mut self: Pin<&mut Self>,
143        cx: &mut Context<'_>,
144        buf: &mut tokio::io::ReadBuf<'_>,
145    ) -> Poll<Result<(), std::io::Error>> {
146        match &mut *self {
147            ConnectionStream::Tcp(t) => Pin::new(t).poll_read(cx, buf),
148        }
149    }
150}
151
152#[cfg(not(target_os = "fuchsia"))]
153impl tokio::io::AsyncWrite for ConnectionStream {
154    fn poll_write(
155        mut self: Pin<&mut Self>,
156        cx: &mut Context<'_>,
157        buf: &[u8],
158    ) -> Poll<io::Result<usize>> {
159        match &mut *self {
160            ConnectionStream::Tcp(t) => Pin::new(t).poll_write(cx, buf),
161        }
162    }
163
164    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
165        match &mut *self {
166            ConnectionStream::Tcp(t) => Pin::new(t).poll_flush(cx),
167        }
168    }
169
170    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
171        match &mut *self {
172            ConnectionStream::Tcp(t) => Pin::new(t).poll_shutdown(cx),
173        }
174    }
175}
176
177#[cfg(not(target_os = "fuchsia"))]
178struct TcpListenerStream(TcpListener);
179
180#[cfg(not(target_os = "fuchsia"))]
181impl Stream for TcpListenerStream {
182    type Item = Result<TcpStream, std::io::Error>;
183    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
184        let this = self.get_mut();
185        let listener = &mut this.0;
186        match listener.poll_accept(cx) {
187            Poll::Ready(value) => Poll::Ready(Some(value.map(|(stream, _)| stream))),
188            Poll::Pending => Poll::Pending,
189        }
190    }
191}
192
193#[derive(Clone, Debug, Builder)]
194#[builder(pattern = "owned")]
195#[builder(derive(Debug))]
196pub struct OmahaServer {
197    #[builder(default, setter(into))]
198    pub responses_by_appid: ResponseMap,
199    #[builder(default = "make_default_private_keys_for_test()")]
200    pub private_keys: PrivateKeys,
201    #[builder(default = "None")]
202    pub etag_override: Option<String>,
203    #[builder(default)]
204    pub require_cup: bool,
205}
206
207impl OmahaServer {
208    /// Sets the special assertion to make on any future update check requests
209    pub fn set_all_update_check_assertions(&mut self, value: UpdateCheckAssertion) {
210        for response_and_metadata in self.responses_by_appid.values_mut() {
211            response_and_metadata.check_assertion = value;
212        }
213    }
214
215    /// Sets the special assertion to make on any future cohort in requests
216    pub fn set_all_cohort_assertions(&mut self, value: Option<String>) {
217        for response_and_metadata in self.responses_by_appid.values_mut() {
218            response_and_metadata.cohort_assertion = value.clone();
219        }
220    }
221
222    /// Start the server detached, returning the address of the server
223    pub async fn start_and_detach(
224        arc_server: Arc<Mutex<OmahaServer>>,
225        addr: Option<SocketAddr>,
226    ) -> Result<String, Error> {
227        let (addr, _server_task) = OmahaServer::start(arc_server, addr).await?;
228        #[cfg(fasync)]
229        _server_task.expect("no server task found").detach();
230
231        Ok(addr)
232    }
233
234    /// Spawn the server on the current executor, returning the address of the server and
235    /// its Task.
236    #[cfg(all(fasync, target_os = "fuchsia"))]
237    pub async fn start(
238        arc_server: Arc<Mutex<OmahaServer>>,
239        addr: Option<SocketAddr>,
240    ) -> Result<(String, Option<Task<()>>), Error> {
241        let addr = if let Some(a) = addr {
242            a
243        } else {
244            SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)
245        };
246
247        let (connections, addr) = {
248            let listener = TcpListener::bind(&addr)?;
249            let local_addr = listener.local_addr()?;
250            (
251                listener
252                    .accept_stream()
253                    .map_ok(|(conn, _addr)| fuchsia_hyper::TcpStream { stream: conn }),
254                local_addr,
255            )
256        };
257
258        let make_svc = make_service_fn(move |_socket| {
259            let arc_server = Arc::clone(&arc_server);
260            async move {
261                Ok::<_, Infallible>(service_fn(move |req| {
262                    let arc_server = Arc::clone(&arc_server);
263                    async move { handle_request(req, &arc_server).await }
264                }))
265            }
266        });
267
268        let server = Server::builder(from_stream(connections))
269            .executor(fuchsia_hyper::Executor)
270            .serve(make_svc)
271            .unwrap_or_else(|e| panic!("error serving omaha server: {e}"));
272
273        let server_task = fasync::Task::spawn(server);
274        Ok((format!("http://{addr}/"), Some(server_task)))
275    }
276
277    /// Spawn the server on the current executor, returning the address of the server and
278    /// its Task.
279    #[cfg(all(fasync, not(target_os = "fuchsia")))]
280    pub async fn start(
281        arc_server: Arc<Mutex<OmahaServer>>,
282        addr: Option<SocketAddr>,
283    ) -> Result<(String, Option<Task<()>>), Error> {
284        let addr = if let Some(a) = addr {
285            a
286        } else {
287            SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)
288        };
289
290        let make_svc = make_service_fn(move |_socket| {
291            let arc_server = Arc::clone(&arc_server);
292            async {
293                Ok::<_, Infallible>(service_fn(move |req| {
294                    let arc_server = Arc::clone(&arc_server);
295                    async move { handle_request(req, &arc_server).await }
296                }))
297            }
298        });
299
300        let listener = TcpListener::bind(&addr)
301            .await
302            .expect("cannot bind to address");
303
304        let addr = listener.local_addr()?;
305
306        let server = async move {
307            Server::builder(from_stream(
308                TcpListenerStream(listener).map_ok(ConnectionStream::Tcp),
309            ))
310            .executor(fuchsia_hyper::Executor)
311            .serve(make_svc)
312            .await
313            .unwrap_or_else(|e| panic!("error serving omaha server: {e}"));
314        };
315
316        let server_task = fasync::Task::spawn(server);
317        Ok((format!("http://{addr}/"), Some(server_task)))
318    }
319
320    /// Spawn the server on the current executor, returning the address of the server and
321    /// its JoinHandle.
322    #[cfg(feature = "tokio")]
323    pub async fn start(
324        arc_server: Arc<Mutex<OmahaServer>>,
325        addr: Option<SocketAddr>,
326    ) -> Result<(String, Option<JoinHandle<()>>), Error> {
327        let addr = if let Some(a) = addr {
328            a
329        } else {
330            SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)
331        };
332
333        let make_svc = make_service_fn(move |_socket| {
334            let arc_server = Arc::clone(&arc_server);
335            async {
336                Ok::<_, Infallible>(service_fn(move |req| {
337                    let arc_server = Arc::clone(&arc_server);
338                    async move { handle_request(req, &arc_server).await }
339                }))
340            }
341        });
342
343        let listener = TcpListener::bind(&addr)
344            .await
345            .expect("cannot bind to address");
346
347        let addr = listener.local_addr()?;
348
349        let server_task = tokio::spawn(async move {
350            let connections = TcpListenerStream(listener).map_ok(ConnectionStream::Tcp);
351            let _ = Server::builder(from_stream(connections))
352                .serve(make_svc)
353                .await;
354        });
355        Ok((format!("http://{addr}/"), Some(server_task)))
356    }
357}
358
359fn make_etag(
360    request_body: &Bytes,
361    uri: &str,
362    private_keys: &PrivateKeys,
363    response_data: &[u8],
364) -> Option<String> {
365    use p256::ecdsa::signature::Signer;
366
367    if uri == "/" {
368        return None;
369    }
370
371    let parsed_uri = Url::parse(&format!("https://example.com{uri}")).unwrap();
372    let mut query_pairs = parsed_uri.query_pairs();
373
374    let (cup2key_key, cup2key_val) = query_pairs.next().unwrap();
375    assert_eq!(cup2key_key, "cup2key");
376
377    let (public_key_id_str, _nonce_str) = cup2key_val.split_once(':').unwrap();
378    let public_key_id: PublicKeyId = public_key_id_str.parse().unwrap();
379    let private_key: &PrivateKey = match private_keys.find(public_key_id) {
380        Some(pk) => Some(pk),
381        None => {
382            log::error!(
383                "Could not find public_key_id {:?} in the private_keys map, which only knows about the latest key_id {:?} and the historical key_ids {:?}",
384                public_key_id,
385                private_keys.latest.id,
386                private_keys.historical.iter().map(|pkid| pkid.id).collect::<Vec<_>>(),
387                );
388            None
389        }
390    }?;
391
392    let request_hash = Sha256::digest(request_body);
393    let response_hash = Sha256::digest(response_data);
394
395    let mut hasher = Sha256::new();
396    hasher.update(request_hash);
397    hasher.update(response_hash);
398    hasher.update(&*cup2key_val);
399    let transaction_hash = hasher.finalize();
400
401    Some(format!(
402        "{}:{}",
403        hex::encode(private_key.sign(&transaction_hash).to_der()),
404        hex::encode(request_hash)
405    ))
406}
407
408pub async fn handle_request(
409    req: Request<Body>,
410    omaha_server: &Mutex<OmahaServer>,
411) -> Result<Response<Body>, Error> {
412    log::debug!("{req:#?}");
413    if req.uri().path() == "/set_responses_by_appid" {
414        return handle_set_responses(req, omaha_server).await;
415    }
416
417    handle_omaha_request(req, omaha_server).await
418}
419
420pub async fn handle_set_responses(
421    req: Request<Body>,
422    omaha_server: &Mutex<OmahaServer>,
423) -> Result<Response<Body>, Error> {
424    assert_eq!(req.method(), Method::POST);
425
426    let req_body = hyper::body::to_bytes(req).await?;
427    let req_json: HashMap<String, ResponseAndMetadata> =
428        serde_json::from_slice(&req_body).expect("parse json");
429    {
430        #[cfg(feature = "tokio")]
431        let mut omaha_server = omaha_server.lock().await;
432        #[cfg(fasync)]
433        let mut omaha_server = omaha_server.lock();
434        omaha_server.responses_by_appid = req_json;
435    }
436
437    let builder = Response::builder()
438        .status(StatusCode::OK)
439        .header(header::CONTENT_LENGTH, 0);
440    Ok(builder.body(Body::empty()).unwrap())
441}
442
443pub async fn handle_omaha_request(
444    req: Request<Body>,
445    omaha_server: &Mutex<OmahaServer>,
446) -> Result<Response<Body>, Error> {
447    #[cfg(feature = "tokio")]
448    let omaha_server = omaha_server.lock().await.clone();
449    #[cfg(fasync)]
450    let omaha_server = omaha_server.lock().clone();
451    assert_eq!(req.method(), Method::POST);
452
453    if omaha_server.responses_by_appid.is_empty() {
454        let builder = Response::builder()
455            .status(StatusCode::INTERNAL_SERVER_ERROR)
456            .header(header::CONTENT_LENGTH, 0);
457        log::error!("Received a request before |responses_by_appid| was set; returning an empty response with status 500.");
458        return Ok(builder.body(Body::empty()).unwrap());
459    }
460
461    let uri_string = req.uri().to_string();
462
463    let req_body = hyper::body::to_bytes(req).await?;
464    let req_json: serde_json::Value = serde_json::from_slice(&req_body).expect("parse json");
465
466    let request = req_json.get("request").unwrap();
467    let apps = request.get("app").unwrap().as_array().unwrap();
468
469    // If this request contains updatecheck, make sure the mock has the right number of configured apps.
470    match apps
471        .iter()
472        .filter(|app| app.get("updatecheck").is_some())
473        .count()
474    {
475        0 => {}
476        x => assert_eq!(x, omaha_server.responses_by_appid.len()),
477    }
478
479    let apps: Vec<serde_json::Value> = apps
480        .iter()
481        .map(|app| {
482            let appid = app.get("appid").unwrap();
483            let expected = &omaha_server.responses_by_appid[appid.as_str().unwrap()];
484
485            if let Some(expected_version) = &expected.version {
486                let version = app.get("version").unwrap();
487                assert_eq!(version, expected_version);
488            }
489
490            let app = if let Some(expected_update_check) = app.get("updatecheck") {
491                let updatedisabled = expected_update_check
492                    .get("updatedisabled")
493                    .map(|v| v.as_bool().unwrap())
494                    .unwrap_or(false);
495                match expected.check_assertion {
496                    UpdateCheckAssertion::UpdatesEnabled => {
497                        assert!(!updatedisabled);
498                    }
499                    UpdateCheckAssertion::UpdatesDisabled => {
500                        assert!(updatedisabled);
501                    }
502                }
503
504                if let Some(cohort_assertion) = &expected.cohort_assertion {
505                    assert_eq!(
506                        app.get("cohort")
507                            .expect("expected cohort")
508                            .as_str()
509                            .expect("cohort is string"),
510                        cohort_assertion
511                    );
512                }
513
514                let updatecheck = match expected.response {
515                    OmahaResponse::Update => json!({
516                        "status": "ok",
517                        "urls": {
518                            "url": [
519                                {
520                                    "codebase": expected.codebase,
521                                }
522                            ]
523                        },
524                        "manifest": {
525                            "version": "0.1.2.3",
526                            "actions": {
527                                "action": [
528                                    {
529                                        "run": &expected.package_name,
530                                        "event": "install"
531                                    },
532                                    {
533                                        "event": "postinstall"
534                                    }
535                                ]
536                            },
537                            "packages": {
538                                "package": [
539                                    {
540                                        "name": &expected.package_name,
541                                        "fp": "2.0.1.2.3",
542                                        "required": true
543                                    }
544                                ]
545                            }
546                        }
547                    }),
548                    OmahaResponse::UrgentUpdate => json!({
549                        "status": "ok",
550                        "urls": {
551                            "url": [
552                                {
553                                    "codebase": expected.codebase,
554                                }
555                            ]
556                        },
557                        "manifest": {
558                            "version": "0.1.2.3",
559                            "actions": {
560                                "action": [
561                                    {
562                                        "run": &expected.package_name,
563                                        "event": "install"
564                                    },
565                                    {
566                                        "event": "postinstall"
567                                    }
568                                ]
569                            },
570                            "packages": {
571                                "package": [
572                                    {
573                                        "name": &expected.package_name,
574                                        "fp": "2.0.1.2.3",
575                                        "required": true
576                                    }
577                                ]
578                            }
579                        },
580                        "_urgent_update": true
581                    }),
582                    OmahaResponse::NoUpdate => json!({
583                        "status": "noupdate",
584                    }),
585                    OmahaResponse::InvalidResponse => json!({
586                        "invalid_status": "invalid",
587                    }),
588                    OmahaResponse::InvalidURL => json!({
589                        "status": "ok",
590                        "urls": {
591                            "url": [
592                                {
593                                    "codebase": "http://integration.test.fuchsia.com/"
594                                }
595                            ]
596                        },
597                        "manifest": {
598                            "version": "0.1.2.3",
599                            "actions": {
600                                "action": [
601                                    {
602                                        "run": &expected.package_name,
603                                        "event": "install"
604                                    },
605                                    {
606                                        "event": "postinstall"
607                                    }
608                                ]
609                            },
610                            "packages": {
611                                "package": [
612                                    {
613                                        "name": &expected.package_name,
614                                        "fp": "2.0.1.2.3",
615                                        "required": true
616                                    }
617                                ]
618                            }
619                        }
620                    }),
621                };
622                json!(
623                {
624                    "cohorthint": "integration-test",
625                    "appid": appid,
626                    "cohort": "1:1:",
627                    "status": "ok",
628                    "cohortname": "integration-test",
629                    "updatecheck": updatecheck,
630                })
631            } else {
632                assert!(app.get("event").is_some());
633                json!(
634                {
635                    "cohorthint": "integration-test",
636                    "appid": appid,
637                    "cohort": "1:1:",
638                    "status": "ok",
639                    "cohortname": "integration-test",
640                })
641            };
642            app
643        })
644        .collect();
645    let response = json!({
646        "response": {
647            "server": "prod",
648            "protocol": "3.0",
649            "daystart": {
650                "elapsed_seconds": 48810,
651                "elapsed_days": 4775
652            },
653            "app": apps
654        }
655    });
656
657    let response_data: Vec<u8> = serde_json::to_vec(&response).unwrap();
658
659    let mut builder = Response::builder()
660        .status(StatusCode::OK)
661        .header(header::CONTENT_LENGTH, response_data.len());
662
663    // It is only possible to calculate an induced etag if the incoming request
664    // had a valid cup2key query argument.
665    let induced_etag: Option<String> = make_etag(
666        &req_body,
667        &uri_string,
668        &omaha_server.private_keys,
669        &response_data,
670    );
671
672    if omaha_server.require_cup && induced_etag.is_none() {
673        panic!(
674            "mock-omaha-server was configured to expect CUP, but we received a request without it."
675        );
676    }
677
678    if let Some(etag) = omaha_server
679        .etag_override
680        .as_ref()
681        .or(induced_etag.as_ref())
682    {
683        builder = builder.header(header::ETAG, etag);
684    }
685
686    Ok(builder.body(Body::from(response_data)).unwrap())
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use anyhow::Context;
693    #[cfg(fasync)]
694    use fuchsia_async as fasync;
695    #[cfg(feature = "tokio")]
696    use hyper::client::HttpConnector;
697    use hyper::Client;
698
699    #[cfg(fasync)]
700    async fn new_http_client() -> Client<fuchsia_hyper::HyperConnector> {
701        fuchsia_hyper::new_client()
702    }
703
704    #[cfg(feature = "tokio")]
705    async fn new_http_client() -> Client<HttpConnector> {
706        Client::new()
707    }
708
709    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
710    #[cfg_attr(feature = "tokio", tokio::test)]
711    async fn test_no_validate_version() -> Result<(), Error> {
712        // Send a request with no specified version and assert that we don't check.
713        // See 0.0.0.1 vs 9.9.9.9 below.
714        let server = OmahaServer::start_and_detach(
715            Arc::new(Mutex::new(
716                OmahaServerBuilder::default()
717                    .responses_by_appid([(
718                        "integration-test-appid-1".to_string(),
719                        ResponseAndMetadata {
720                            response: OmahaResponse::NoUpdate,
721                            version: None,
722                            ..Default::default()
723                        },
724                    )])
725                    .build()
726                    .unwrap(),
727            )),
728            None,
729        )
730        .await
731        .context("starting server")?;
732
733        let client = new_http_client().await;
734        let body = json!({
735            "request": {
736                "app": [
737                    {
738                        "appid": "integration-test-appid-1",
739                        "version": "9.9.9.9",
740                        "updatecheck": { "updatedisabled": false }
741                    },
742                ]
743            }
744        });
745        let request = Request::post(&server)
746            .body(Body::from(body.to_string()))
747            .unwrap();
748
749        let response = client.request(request).await?;
750
751        assert_eq!(response.status(), StatusCode::OK);
752        let body = hyper::body::to_bytes(response)
753            .await
754            .context("reading response body")?;
755        let obj: serde_json::Value =
756            serde_json::from_slice(&body).context("parsing response json")?;
757
758        let response = obj.get("response").unwrap();
759        let apps = response.get("app").unwrap().as_array().unwrap();
760        assert_eq!(apps.len(), 1);
761        let status = apps[0].get("updatecheck").unwrap().get("status").unwrap();
762        assert_eq!(status, "noupdate");
763        Ok(())
764    }
765
766    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
767    #[cfg_attr(feature = "tokio", tokio::test)]
768    async fn test_server_replies() -> Result<(), Error> {
769        let server_url = OmahaServer::start_and_detach(
770            Arc::new(Mutex::new(
771                OmahaServerBuilder::default()
772                    .responses_by_appid([
773                        (
774                            "integration-test-appid-1".to_string(),
775                            ResponseAndMetadata {
776                                response: OmahaResponse::NoUpdate,
777                                version: Some("0.0.0.1".to_string()),
778                                ..Default::default()
779                            },
780                        ),
781                        (
782                            "integration-test-appid-2".to_string(),
783                            ResponseAndMetadata {
784                                response: OmahaResponse::NoUpdate,
785                                version: Some("0.0.0.2".to_string()),
786                                ..Default::default()
787                            },
788                        ),
789                    ])
790                    .build()
791                    .unwrap(),
792            )),
793            None,
794        )
795        .await
796        .context("starting server")?;
797
798        {
799            let client = new_http_client().await;
800            let body = json!({
801                "request": {
802                    "app": [
803                        {
804                            "appid": "integration-test-appid-1",
805                            "version": "0.0.0.1",
806                            "updatecheck": { "updatedisabled": false }
807                        },
808                        {
809                            "appid": "integration-test-appid-2",
810                            "version": "0.0.0.2",
811                            "updatecheck": { "updatedisabled": false }
812                        },
813                    ]
814                }
815            });
816            let request = Request::post(&server_url)
817                .body(Body::from(body.to_string()))
818                .unwrap();
819
820            let response = client.request(request).await?;
821
822            assert_eq!(response.status(), StatusCode::OK);
823            let body = hyper::body::to_bytes(response)
824                .await
825                .context("reading response body")?;
826            let obj: serde_json::Value =
827                serde_json::from_slice(&body).context("parsing response json")?;
828
829            let response = obj.get("response").unwrap();
830            let apps = response.get("app").unwrap().as_array().unwrap();
831            assert_eq!(apps.len(), 2);
832            for app in apps {
833                let status = app.get("updatecheck").unwrap().get("status").unwrap();
834                assert_eq!(status, "noupdate");
835            }
836        }
837
838        {
839            // change the expected responses; now we only configure one app,
840            // 'integration-test-appid-1', which will respond with an update.
841            let body = json!({
842                "integration-test-appid-1": {
843                    "response": "Update",
844                    "check_assertion": "UpdatesEnabled",
845                    "version": "0.0.0.1",
846                    "codebase": "fuchsia-pkg://integration.test.fuchsia.com/",
847                    "package_name": "update?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
848                }
849            });
850            let request = Request::post(format!("{server_url}set_responses_by_appid"))
851                .body(Body::from(body.to_string()))
852                .unwrap();
853            let client = new_http_client().await;
854            let response = client.request(request).await?;
855            assert_eq!(response.status(), StatusCode::OK);
856        }
857
858        {
859            let body = json!({
860                "request": {
861                    "app": [
862                        {
863                            "appid": "integration-test-appid-1",
864                            "version": "0.0.0.1",
865                            "updatecheck": { "updatedisabled": false }
866                        },
867                    ]
868                }
869            });
870            let request = Request::post(&server_url)
871                .body(Body::from(body.to_string()))
872                .unwrap();
873
874            let client = new_http_client().await;
875            let response = client.request(request).await?;
876
877            assert_eq!(response.status(), StatusCode::OK);
878            let body = hyper::body::to_bytes(response)
879                .await
880                .context("reading response body")?;
881            let obj: serde_json::Value =
882                serde_json::from_slice(&body).context("parsing response json")?;
883
884            let response = obj.get("response").unwrap();
885            let apps = response.get("app").unwrap().as_array().unwrap();
886            assert_eq!(apps.len(), 1);
887            for app in apps {
888                let status = app.get("updatecheck").unwrap().get("status").unwrap();
889                // We configured 'integration-test-appid-1' to respond with an update.
890                assert_eq!(status, "ok");
891            }
892        }
893
894        Ok(())
895    }
896
897    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
898    #[cfg_attr(feature = "tokio", tokio::test)]
899    async fn test_no_configured_responses() -> Result<(), Error> {
900        let server = OmahaServer::start_and_detach(
901            Arc::new(Mutex::new(
902                OmahaServerBuilder::default()
903                    .responses_by_appid([])
904                    .build()
905                    .unwrap(),
906            )),
907            None,
908        )
909        .await
910        .context("starting server")?;
911
912        let client = new_http_client().await;
913        let body = json!({
914            "request": {
915                "app": [
916                    {
917                        "appid": "integration-test-appid-1",
918                        "version": "0.1.2.3",
919                        "updatecheck": { "updatedisabled": false }
920                    },
921                ]
922            }
923        });
924        let request = Request::post(&server)
925            .body(Body::from(body.to_string()))
926            .unwrap();
927        let response = client.request(request).await?;
928        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
929        Ok(())
930    }
931
932    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
933    #[cfg_attr(feature = "tokio", tokio::test)]
934    async fn test_server_expect_cup_nopanic() -> Result<(), Error> {
935        let server_url = OmahaServer::start_and_detach(
936            Arc::new(Mutex::new(
937                OmahaServerBuilder::default()
938                    .responses_by_appid([(
939                        "integration-test-appid-1".to_string(),
940                        ResponseAndMetadata {
941                            response: OmahaResponse::NoUpdate,
942                            version: Some("0.0.0.1".to_string()),
943                            ..Default::default()
944                        },
945                    )])
946                    .require_cup(true)
947                    .build()
948                    .unwrap(),
949            )),
950            None,
951        )
952        .await
953        .context("starting server")?;
954
955        let client = new_http_client().await;
956        let body = json!({
957            "request": {
958                "app": [
959                    {
960                        "appid": "integration-test-appid-1",
961                        "version": "0.0.0.1",
962                        "updatecheck": { "updatedisabled": false }
963                    },
964                ]
965            }
966        });
967        // CUP attached.
968        let request = Request::post(format!(
969            "{}?cup2key={}:nonce",
970            &server_url,
971            make_default_public_key_id_for_test()
972        ))
973        .body(Body::from(body.to_string()))
974        .unwrap();
975
976        let response = client.request(request).await?;
977
978        assert_eq!(response.status(), StatusCode::OK);
979        Ok(())
980    }
981
982    #[cfg_attr(
983        fasync,
984        fasync::run_singlethreaded(test),
985        should_panic(expected = "configured to expect CUP")
986    )]
987    #[cfg_attr(
988        feature = "tokio",
989        tokio::test,
990        should_panic(
991            expected = "called `Result::unwrap()` on an `Err` value: hyper::Error(IncompleteMessage)"
992        )
993    )]
994    async fn test_server_expect_cup_panic() {
995        let server_url = OmahaServer::start_and_detach(
996            Arc::new(Mutex::new(
997                OmahaServerBuilder::default()
998                    .responses_by_appid([(
999                        "integration-test-appid-1".to_string(),
1000                        ResponseAndMetadata {
1001                            response: OmahaResponse::NoUpdate,
1002                            version: Some("0.0.0.1".to_string()),
1003                            ..Default::default()
1004                        },
1005                    )])
1006                    .require_cup(true)
1007                    .build()
1008                    .unwrap(),
1009            )),
1010            None,
1011        )
1012        .await
1013        .context("starting server")
1014        .unwrap();
1015
1016        let client = new_http_client().await;
1017        let body = json!({
1018            "request": {
1019                "app": [
1020                    {
1021                        "appid": "integration-test-appid-1",
1022                        "version": "0.0.0.1",
1023                        "updatecheck": { "updatedisabled": false }
1024                    },
1025                ]
1026            }
1027        });
1028        // no CUP, but we set .require_cup(true) above, so mock-omaha-server will
1029        // panic. (See should_panic above.)
1030        let request = Request::post(&server_url)
1031            .body(Body::from(body.to_string()))
1032            .unwrap();
1033        let _response = client.request(request).await.unwrap();
1034    }
1035}