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 futures::prelude::*;
12use hyper::body::Bytes;
13use hyper::server::Server;
14use hyper::server::accept::from_stream;
15use hyper::service::{make_service_fn, service_fn};
16use hyper::{Body, Method, Request, Response, StatusCode, header};
17use omaha_client::cup_ecdsa::PublicKeyId;
18use omaha_client::cup_ecdsa::test_support::{
19    make_default_private_key_for_test, make_default_public_key_id_for_test,
20};
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
387                    .historical
388                    .iter()
389                    .map(|pkid| pkid.id)
390                    .collect::<Vec<_>>(),
391            );
392            None
393        }
394    }?;
395
396    let request_hash = Sha256::digest(request_body);
397    let response_hash = Sha256::digest(response_data);
398
399    let mut hasher = Sha256::new();
400    hasher.update(request_hash);
401    hasher.update(response_hash);
402    hasher.update(&*cup2key_val);
403    let transaction_hash = hasher.finalize();
404
405    Some(format!(
406        "{}:{}",
407        hex::encode(private_key.sign(&transaction_hash).to_der()),
408        hex::encode(request_hash)
409    ))
410}
411
412pub async fn handle_request(
413    req: Request<Body>,
414    omaha_server: &Mutex<OmahaServer>,
415) -> Result<Response<Body>, Error> {
416    log::debug!("{req:#?}");
417    if req.uri().path() == "/set_responses_by_appid" {
418        return handle_set_responses(req, omaha_server).await;
419    }
420
421    handle_omaha_request(req, omaha_server).await
422}
423
424pub async fn handle_set_responses(
425    req: Request<Body>,
426    omaha_server: &Mutex<OmahaServer>,
427) -> Result<Response<Body>, Error> {
428    assert_eq!(req.method(), Method::POST);
429
430    let req_body = hyper::body::to_bytes(req).await?;
431    let req_json: HashMap<String, ResponseAndMetadata> =
432        serde_json::from_slice(&req_body).expect("parse json");
433    {
434        #[cfg(feature = "tokio")]
435        let mut omaha_server = omaha_server.lock().await;
436        #[cfg(fasync)]
437        let mut omaha_server = omaha_server.lock();
438        omaha_server.responses_by_appid = req_json;
439    }
440
441    let builder = Response::builder()
442        .status(StatusCode::OK)
443        .header(header::CONTENT_LENGTH, 0);
444    Ok(builder.body(Body::empty()).unwrap())
445}
446
447pub async fn handle_omaha_request(
448    req: Request<Body>,
449    omaha_server: &Mutex<OmahaServer>,
450) -> Result<Response<Body>, Error> {
451    #[cfg(feature = "tokio")]
452    let omaha_server = omaha_server.lock().await.clone();
453    #[cfg(fasync)]
454    let omaha_server = omaha_server.lock().clone();
455    assert_eq!(req.method(), Method::POST);
456
457    if omaha_server.responses_by_appid.is_empty() {
458        let builder = Response::builder()
459            .status(StatusCode::INTERNAL_SERVER_ERROR)
460            .header(header::CONTENT_LENGTH, 0);
461        log::error!(
462            "Received a request before |responses_by_appid| was set; returning an empty response with status 500."
463        );
464        return Ok(builder.body(Body::empty()).unwrap());
465    }
466
467    let uri_string = req.uri().to_string();
468
469    let req_body = hyper::body::to_bytes(req).await?;
470    let req_json: serde_json::Value = serde_json::from_slice(&req_body).expect("parse json");
471
472    let request = req_json.get("request").unwrap();
473    let apps = request.get("app").unwrap().as_array().unwrap();
474
475    // If this request contains updatecheck, make sure the mock has the right number of configured apps.
476    match apps
477        .iter()
478        .filter(|app| app.get("updatecheck").is_some())
479        .count()
480    {
481        0 => {}
482        x => assert_eq!(x, omaha_server.responses_by_appid.len()),
483    }
484
485    let apps: Vec<serde_json::Value> = apps
486        .iter()
487        .map(|app| {
488            let appid = app.get("appid").unwrap();
489            let expected = &omaha_server.responses_by_appid[appid.as_str().unwrap()];
490
491            if let Some(expected_version) = &expected.version {
492                let version = app.get("version").unwrap();
493                assert_eq!(version, expected_version);
494            }
495
496            if let Some(expected_update_check) = app.get("updatecheck") {
497                let updatedisabled = expected_update_check
498                    .get("updatedisabled")
499                    .map(|v| v.as_bool().unwrap())
500                    .unwrap_or(false);
501                match expected.check_assertion {
502                    UpdateCheckAssertion::UpdatesEnabled => {
503                        assert!(!updatedisabled);
504                    }
505                    UpdateCheckAssertion::UpdatesDisabled => {
506                        assert!(updatedisabled);
507                    }
508                }
509
510                if let Some(cohort_assertion) = &expected.cohort_assertion {
511                    assert_eq!(
512                        app.get("cohort")
513                            .expect("expected cohort")
514                            .as_str()
515                            .expect("cohort is string"),
516                        cohort_assertion
517                    );
518                }
519
520                let updatecheck = match expected.response {
521                    OmahaResponse::Update => json!({
522                        "status": "ok",
523                        "urls": {
524                            "url": [
525                                {
526                                    "codebase": expected.codebase,
527                                }
528                            ]
529                        },
530                        "manifest": {
531                            "version": "0.1.2.3",
532                            "actions": {
533                                "action": [
534                                    {
535                                        "run": &expected.package_name,
536                                        "event": "install"
537                                    },
538                                    {
539                                        "event": "postinstall"
540                                    }
541                                ]
542                            },
543                            "packages": {
544                                "package": [
545                                    {
546                                        "name": &expected.package_name,
547                                        "fp": "2.0.1.2.3",
548                                        "required": true
549                                    }
550                                ]
551                            }
552                        }
553                    }),
554                    OmahaResponse::UrgentUpdate => json!({
555                        "status": "ok",
556                        "urls": {
557                            "url": [
558                                {
559                                    "codebase": expected.codebase,
560                                }
561                            ]
562                        },
563                        "manifest": {
564                            "version": "0.1.2.3",
565                            "actions": {
566                                "action": [
567                                    {
568                                        "run": &expected.package_name,
569                                        "event": "install"
570                                    },
571                                    {
572                                        "event": "postinstall"
573                                    }
574                                ]
575                            },
576                            "packages": {
577                                "package": [
578                                    {
579                                        "name": &expected.package_name,
580                                        "fp": "2.0.1.2.3",
581                                        "required": true
582                                    }
583                                ]
584                            }
585                        },
586                        "_urgent_update": true
587                    }),
588                    OmahaResponse::NoUpdate => json!({
589                        "status": "noupdate",
590                    }),
591                    OmahaResponse::InvalidResponse => json!({
592                        "invalid_status": "invalid",
593                    }),
594                    OmahaResponse::InvalidURL => json!({
595                        "status": "ok",
596                        "urls": {
597                            "url": [
598                                {
599                                    "codebase": "http://integration.test.fuchsia.com/"
600                                }
601                            ]
602                        },
603                        "manifest": {
604                            "version": "0.1.2.3",
605                            "actions": {
606                                "action": [
607                                    {
608                                        "run": &expected.package_name,
609                                        "event": "install"
610                                    },
611                                    {
612                                        "event": "postinstall"
613                                    }
614                                ]
615                            },
616                            "packages": {
617                                "package": [
618                                    {
619                                        "name": &expected.package_name,
620                                        "fp": "2.0.1.2.3",
621                                        "required": true
622                                    }
623                                ]
624                            }
625                        }
626                    }),
627                };
628                json!(
629                {
630                    "cohorthint": "integration-test",
631                    "appid": appid,
632                    "cohort": "1:1:",
633                    "status": "ok",
634                    "cohortname": "integration-test",
635                    "updatecheck": updatecheck,
636                })
637            } else {
638                assert!(app.get("event").is_some());
639                json!(
640                {
641                    "cohorthint": "integration-test",
642                    "appid": appid,
643                    "cohort": "1:1:",
644                    "status": "ok",
645                    "cohortname": "integration-test",
646                })
647            }
648        })
649        .collect();
650    let response = json!({
651        "response": {
652            "server": "prod",
653            "protocol": "3.0",
654            "daystart": {
655                "elapsed_seconds": 48810,
656                "elapsed_days": 4775
657            },
658            "app": apps
659        }
660    });
661
662    let response_data: Vec<u8> = serde_json::to_vec(&response).unwrap();
663
664    let mut builder = Response::builder()
665        .status(StatusCode::OK)
666        .header(header::CONTENT_LENGTH, response_data.len());
667
668    // It is only possible to calculate an induced etag if the incoming request
669    // had a valid cup2key query argument.
670    let induced_etag: Option<String> = make_etag(
671        &req_body,
672        &uri_string,
673        &omaha_server.private_keys,
674        &response_data,
675    );
676
677    if omaha_server.require_cup && induced_etag.is_none() {
678        panic!(
679            "mock-omaha-server was configured to expect CUP, but we received a request without it."
680        );
681    }
682
683    if let Some(etag) = omaha_server
684        .etag_override
685        .as_ref()
686        .or(induced_etag.as_ref())
687    {
688        builder = builder.header(header::ETAG, etag);
689    }
690
691    Ok(builder.body(Body::from(response_data)).unwrap())
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use anyhow::Context;
698    #[cfg(fasync)]
699    use fuchsia_async as fasync;
700    use hyper::Client;
701    #[cfg(feature = "tokio")]
702    use hyper::client::HttpConnector;
703
704    #[cfg(fasync)]
705    async fn new_http_client() -> Client<fuchsia_hyper::HyperConnector> {
706        fuchsia_hyper::new_client()
707    }
708
709    #[cfg(feature = "tokio")]
710    async fn new_http_client() -> Client<HttpConnector> {
711        Client::new()
712    }
713
714    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
715    #[cfg_attr(feature = "tokio", tokio::test)]
716    async fn test_no_validate_version() -> Result<(), Error> {
717        // Send a request with no specified version and assert that we don't check.
718        // See 0.0.0.1 vs 9.9.9.9 below.
719        let server = OmahaServer::start_and_detach(
720            Arc::new(Mutex::new(
721                OmahaServerBuilder::default()
722                    .responses_by_appid([(
723                        "integration-test-appid-1".to_string(),
724                        ResponseAndMetadata {
725                            response: OmahaResponse::NoUpdate,
726                            version: None,
727                            ..Default::default()
728                        },
729                    )])
730                    .build()
731                    .unwrap(),
732            )),
733            None,
734        )
735        .await
736        .context("starting server")?;
737
738        let client = new_http_client().await;
739        let body = json!({
740            "request": {
741                "app": [
742                    {
743                        "appid": "integration-test-appid-1",
744                        "version": "9.9.9.9",
745                        "updatecheck": { "updatedisabled": false }
746                    },
747                ]
748            }
749        });
750        let request = Request::post(&server)
751            .body(Body::from(body.to_string()))
752            .unwrap();
753
754        let response = client.request(request).await?;
755
756        assert_eq!(response.status(), StatusCode::OK);
757        let body = hyper::body::to_bytes(response)
758            .await
759            .context("reading response body")?;
760        let obj: serde_json::Value =
761            serde_json::from_slice(&body).context("parsing response json")?;
762
763        let response = obj.get("response").unwrap();
764        let apps = response.get("app").unwrap().as_array().unwrap();
765        assert_eq!(apps.len(), 1);
766        let status = apps[0].get("updatecheck").unwrap().get("status").unwrap();
767        assert_eq!(status, "noupdate");
768        Ok(())
769    }
770
771    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
772    #[cfg_attr(feature = "tokio", tokio::test)]
773    async fn test_server_replies() -> Result<(), Error> {
774        let server_url = OmahaServer::start_and_detach(
775            Arc::new(Mutex::new(
776                OmahaServerBuilder::default()
777                    .responses_by_appid([
778                        (
779                            "integration-test-appid-1".to_string(),
780                            ResponseAndMetadata {
781                                response: OmahaResponse::NoUpdate,
782                                version: Some("0.0.0.1".to_string()),
783                                ..Default::default()
784                            },
785                        ),
786                        (
787                            "integration-test-appid-2".to_string(),
788                            ResponseAndMetadata {
789                                response: OmahaResponse::NoUpdate,
790                                version: Some("0.0.0.2".to_string()),
791                                ..Default::default()
792                            },
793                        ),
794                    ])
795                    .build()
796                    .unwrap(),
797            )),
798            None,
799        )
800        .await
801        .context("starting server")?;
802
803        {
804            let client = new_http_client().await;
805            let body = json!({
806                "request": {
807                    "app": [
808                        {
809                            "appid": "integration-test-appid-1",
810                            "version": "0.0.0.1",
811                            "updatecheck": { "updatedisabled": false }
812                        },
813                        {
814                            "appid": "integration-test-appid-2",
815                            "version": "0.0.0.2",
816                            "updatecheck": { "updatedisabled": false }
817                        },
818                    ]
819                }
820            });
821            let request = Request::post(&server_url)
822                .body(Body::from(body.to_string()))
823                .unwrap();
824
825            let response = client.request(request).await?;
826
827            assert_eq!(response.status(), StatusCode::OK);
828            let body = hyper::body::to_bytes(response)
829                .await
830                .context("reading response body")?;
831            let obj: serde_json::Value =
832                serde_json::from_slice(&body).context("parsing response json")?;
833
834            let response = obj.get("response").unwrap();
835            let apps = response.get("app").unwrap().as_array().unwrap();
836            assert_eq!(apps.len(), 2);
837            for app in apps {
838                let status = app.get("updatecheck").unwrap().get("status").unwrap();
839                assert_eq!(status, "noupdate");
840            }
841        }
842
843        {
844            // change the expected responses; now we only configure one app,
845            // 'integration-test-appid-1', which will respond with an update.
846            let body = json!({
847                "integration-test-appid-1": {
848                    "response": "Update",
849                    "check_assertion": "UpdatesEnabled",
850                    "version": "0.0.0.1",
851                    "codebase": "fuchsia-pkg://integration.test.fuchsia.com/",
852                    "package_name": "update?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
853                }
854            });
855            let request = Request::post(format!("{server_url}set_responses_by_appid"))
856                .body(Body::from(body.to_string()))
857                .unwrap();
858            let client = new_http_client().await;
859            let response = client.request(request).await?;
860            assert_eq!(response.status(), StatusCode::OK);
861        }
862
863        {
864            let body = json!({
865                "request": {
866                    "app": [
867                        {
868                            "appid": "integration-test-appid-1",
869                            "version": "0.0.0.1",
870                            "updatecheck": { "updatedisabled": false }
871                        },
872                    ]
873                }
874            });
875            let request = Request::post(&server_url)
876                .body(Body::from(body.to_string()))
877                .unwrap();
878
879            let client = new_http_client().await;
880            let response = client.request(request).await?;
881
882            assert_eq!(response.status(), StatusCode::OK);
883            let body = hyper::body::to_bytes(response)
884                .await
885                .context("reading response body")?;
886            let obj: serde_json::Value =
887                serde_json::from_slice(&body).context("parsing response json")?;
888
889            let response = obj.get("response").unwrap();
890            let apps = response.get("app").unwrap().as_array().unwrap();
891            assert_eq!(apps.len(), 1);
892            for app in apps {
893                let status = app.get("updatecheck").unwrap().get("status").unwrap();
894                // We configured 'integration-test-appid-1' to respond with an update.
895                assert_eq!(status, "ok");
896            }
897        }
898
899        Ok(())
900    }
901
902    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
903    #[cfg_attr(feature = "tokio", tokio::test)]
904    async fn test_no_configured_responses() -> Result<(), Error> {
905        let server = OmahaServer::start_and_detach(
906            Arc::new(Mutex::new(
907                OmahaServerBuilder::default()
908                    .responses_by_appid([])
909                    .build()
910                    .unwrap(),
911            )),
912            None,
913        )
914        .await
915        .context("starting server")?;
916
917        let client = new_http_client().await;
918        let body = json!({
919            "request": {
920                "app": [
921                    {
922                        "appid": "integration-test-appid-1",
923                        "version": "0.1.2.3",
924                        "updatecheck": { "updatedisabled": false }
925                    },
926                ]
927            }
928        });
929        let request = Request::post(&server)
930            .body(Body::from(body.to_string()))
931            .unwrap();
932        let response = client.request(request).await?;
933        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
934        Ok(())
935    }
936
937    #[cfg_attr(fasync, fasync::run_singlethreaded(test))]
938    #[cfg_attr(feature = "tokio", tokio::test)]
939    async fn test_server_expect_cup_nopanic() -> Result<(), Error> {
940        let server_url = OmahaServer::start_and_detach(
941            Arc::new(Mutex::new(
942                OmahaServerBuilder::default()
943                    .responses_by_appid([(
944                        "integration-test-appid-1".to_string(),
945                        ResponseAndMetadata {
946                            response: OmahaResponse::NoUpdate,
947                            version: Some("0.0.0.1".to_string()),
948                            ..Default::default()
949                        },
950                    )])
951                    .require_cup(true)
952                    .build()
953                    .unwrap(),
954            )),
955            None,
956        )
957        .await
958        .context("starting server")?;
959
960        let client = new_http_client().await;
961        let body = json!({
962            "request": {
963                "app": [
964                    {
965                        "appid": "integration-test-appid-1",
966                        "version": "0.0.0.1",
967                        "updatecheck": { "updatedisabled": false }
968                    },
969                ]
970            }
971        });
972        // CUP attached.
973        let request = Request::post(format!(
974            "{}?cup2key={}:nonce",
975            &server_url,
976            make_default_public_key_id_for_test()
977        ))
978        .body(Body::from(body.to_string()))
979        .unwrap();
980
981        let response = client.request(request).await?;
982
983        assert_eq!(response.status(), StatusCode::OK);
984        Ok(())
985    }
986
987    #[cfg_attr(
988        fasync,
989        fasync::run_singlethreaded(test),
990        should_panic(expected = "configured to expect CUP")
991    )]
992    #[cfg_attr(
993        feature = "tokio",
994        tokio::test,
995        should_panic(
996            expected = "called `Result::unwrap()` on an `Err` value: hyper::Error(IncompleteMessage)"
997        )
998    )]
999    async fn test_server_expect_cup_panic() {
1000        let server_url = OmahaServer::start_and_detach(
1001            Arc::new(Mutex::new(
1002                OmahaServerBuilder::default()
1003                    .responses_by_appid([(
1004                        "integration-test-appid-1".to_string(),
1005                        ResponseAndMetadata {
1006                            response: OmahaResponse::NoUpdate,
1007                            version: Some("0.0.0.1".to_string()),
1008                            ..Default::default()
1009                        },
1010                    )])
1011                    .require_cup(true)
1012                    .build()
1013                    .unwrap(),
1014            )),
1015            None,
1016        )
1017        .await
1018        .context("starting server")
1019        .unwrap();
1020
1021        let client = new_http_client().await;
1022        let body = json!({
1023            "request": {
1024                "app": [
1025                    {
1026                        "appid": "integration-test-appid-1",
1027                        "version": "0.0.0.1",
1028                        "updatecheck": { "updatedisabled": false }
1029                    },
1030                ]
1031            }
1032        });
1033        // no CUP, but we set .require_cup(true) above, so mock-omaha-server will
1034        // panic. (See should_panic above.)
1035        let request = Request::post(&server_url)
1036            .body(Body::from(body.to_string()))
1037            .unwrap();
1038        let _response = client.request(request).await.unwrap();
1039    }
1040}