1use 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 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
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 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 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 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 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 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 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 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}