1use 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 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
84pub 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#[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 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 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 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 #[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 #[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 #[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 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 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 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 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 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 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 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}