Skip to main content

fuchsia_pkg_testing/
serve.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Test tools for serving TUF repositories.
6
7use crate::repo::Repository;
8use anyhow::{Context as _, Error, bail, format_err};
9use chrono::Utc;
10use fidl_fuchsia_pkg_ext::{
11    MirrorConfig, MirrorConfigBuilder, RepositoryConfig, RepositoryStorageType,
12};
13use fuchsia_async::net::TcpListener;
14use fuchsia_async::{self as fasync, Task};
15use fuchsia_repo::body::Body;
16use fuchsia_sync::Mutex;
17use fuchsia_url::RepositoryUrl;
18use futures::future::BoxFuture;
19use futures::prelude::*;
20use http::Uri;
21use http_body_util::BodyExt;
22use http_sse::{Event, EventSender, SseResponseCreator};
23use hyper::service::service_fn;
24use hyper::{Method, Request, Response, StatusCode, header};
25use std::convert::{Infallible, TryInto as _};
26use std::io::{Cursor, Read as _, Seek as _};
27use std::net::{IpAddr, Ipv6Addr, SocketAddr};
28use std::path::{Path, PathBuf};
29use std::pin::Pin;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicU64, Ordering};
32use std::time::Duration;
33
34pub mod responder;
35
36trait AsyncReadWrite: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send {}
37impl<T> AsyncReadWrite for T where T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send {}
38
39/// Domains with known keys and certificates for testing.
40pub enum Domain {
41    /// `test.fuchsia.com` and `localhost`.
42    TestFuchsiaCom,
43    /// `*.fuchsia-updates.googleusercontent.com`.
44    WildcardFuchsiaUpdatesGoogleusercontentCom,
45}
46
47/// A builder to construct a test repository server.
48pub struct ServedRepositoryBuilder {
49    repo: Arc<Repository>,
50    response_overriders: Vec<Arc<dyn HttpResponder>>,
51    bind_addr: IpAddr,
52    bind_port: u16,
53    https_domain: Option<Domain>,
54}
55
56/// Override how a `ServedRepository` responds to requests.
57/// Useful for injecting failures.
58pub trait HttpResponder: 'static + Send + Sync {
59    /// `response` is what the server would have responded with.
60    fn respond<'a>(
61        &'a self,
62        request: &'a Request<Body>,
63        response: Response<Body>,
64    ) -> BoxFuture<'a, Response<Body>>;
65}
66
67/// An entry in the server's log of requests.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct HistoryEntry {
70    /// The request's method.
71    pub method: http::Method,
72    /// The request's path.
73    pub path: String,
74    /// The response's status code.
75    pub status: http::StatusCode,
76}
77
78impl ServedRepositoryBuilder {
79    pub(crate) fn new(repo: Arc<Repository>) -> Self {
80        ServedRepositoryBuilder {
81            repo,
82            response_overriders: vec![],
83            bind_addr: Ipv6Addr::UNSPECIFIED.into(),
84            bind_port: 0,
85            https_domain: None,
86        }
87    }
88
89    /// Override how the `ServedRepositoryBuilder` responds to requests.
90    ///
91    /// Requests are passed through responders in the order in which the responders are added to
92    /// this builder.
93    pub fn response_overrider(mut self, responder: impl HttpResponder) -> Self {
94        self.response_overriders.push(Arc::new(responder));
95        self
96    }
97
98    /// Serve the repository over https via a domain known by test certificates
99    /// and keys.
100    pub fn use_https_domain(mut self, domain: Domain) -> Self {
101        self.https_domain = Some(domain);
102        self
103    }
104
105    /// Bind the tcp listener to the provided ip address. Binds to Ipv6Addr::UNSPECIFIED by
106    /// default.
107    pub fn bind_to_addr(mut self, addr: impl Into<IpAddr>) -> Self {
108        self.bind_addr = addr.into();
109        self
110    }
111
112    /// Bind the tcp listener to the provided port. Binds to 0 (allowing system to select a port) by
113    /// default.
114    pub fn bind_to_port(mut self, port: u16) -> Self {
115        self.bind_port = port;
116        self
117    }
118
119    /// Spawn the server on the current executor, returning a handle to manage the server.
120    pub fn start(self) -> Result<ServedRepository, Error> {
121        let (listener, addr) = {
122            let addr = SocketAddr::new(self.bind_addr, self.bind_port);
123            let listener = TcpListener::bind(&addr).context("bind")?;
124            let local_addr = listener.local_addr().context("local_addr")?;
125            (listener, local_addr)
126        };
127
128        let listener = listener
129            .accept_stream()
130            .map_err(Error::from)
131            .map_ok(|(conn, _addr)| fuchsia_hyper::TcpStream { stream: conn });
132
133        let connection_attempts = Arc::new(AtomicU64::new(0));
134        #[allow(clippy::type_complexity)]
135        let connections: Pin<
136            Box<dyn Stream<Item = Result<Pin<Box<dyn AsyncReadWrite>>, Error>> + Send>,
137        > = if let Some(ref https_domain) = self.https_domain {
138            // build a server configuration using a test CA and cert chain
139            let (certs, key) = match https_domain {
140                Domain::TestFuchsiaCom => (
141                    parse_cert_chain(&include_bytes!("../certs/test.fuchsia.com.certchain")[..]),
142                    parse_private_key(&include_bytes!("../certs/test.fuchsia.com.rsa")[..]),
143                ),
144                Domain::WildcardFuchsiaUpdatesGoogleusercontentCom => (
145                    parse_cert_chain(
146                        &include_bytes!(
147                            "../certs/wildcard.fuchsia-updates.googleusercontent.com.certchain"
148                        )[..],
149                    ),
150                    parse_private_key(
151                        &include_bytes!(
152                            "../certs/wildcard.fuchsia-updates.googleusercontent.com.rsa"
153                        )[..],
154                    ),
155                ),
156            };
157            let mut tls_config = rustls::ServerConfig::builder()
158                .with_no_client_auth()
159                .with_single_cert(certs, key)
160                .unwrap();
161
162            // Configure ALPN and prefer H2 over HTTP/1.1.
163            tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
164
165            let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls_config));
166            let connection_attempts = Arc::clone(&connection_attempts);
167
168            // wrap incoming tcp streams
169            listener
170                .and_then(move |conn| {
171                    connection_attempts.fetch_add(1, Ordering::SeqCst);
172                    tls_acceptor.accept(conn).map(|res| match res {
173                        Ok(conn) => Ok(Pin::new(Box::new(conn)) as Pin<Box<dyn AsyncReadWrite>>),
174                        Err(e) => Err(Error::from(e)),
175                    })
176                })
177                .boxed()
178        } else {
179            let connection_attempts = Arc::clone(&connection_attempts);
180            listener
181                .map_ok(move |conn| {
182                    connection_attempts.fetch_add(1, Ordering::SeqCst);
183                    Pin::new(Box::new(conn)) as Pin<Box<dyn AsyncReadWrite>>
184                })
185                .boxed()
186        };
187
188        let root = self.repo.path();
189        let response_overriders = Arc::new(self.response_overriders);
190
191        let (auto_response_creator, auto_event_sender) =
192            SseResponseCreator::with_additional_buffer_size(10);
193        let auto_response_creator = Arc::new(auto_response_creator);
194
195        let history = Arc::new(Mutex::new(vec![]));
196        let history_clone = Arc::clone(&history);
197
198        let (stop, rx_stop) = futures::channel::oneshot::channel();
199        let rx_stop = rx_stop.map(|res| res.unwrap_or(())).shared();
200
201        let server = Task::spawn(async move {
202            let mut connections = connections.fuse();
203            let mut tasks = futures::stream::FuturesUnordered::new();
204            loop {
205                futures::select! {
206                    conn = connections.next() => {
207                        match conn {
208                            Some(Ok(conn)) => {
209                                let root = root.clone();
210                                let response_overriders = Arc::clone(&response_overriders);
211                                let auto_response_creator = Arc::clone(&auto_response_creator);
212                                let history = Arc::clone(&history_clone);
213                                let rx_stop = rx_stop.clone();
214                                tasks.push(fuchsia_async::Task::spawn(async move {
215                                    let service = service_fn(move |req: Request<hyper::body::Incoming>| {
216                                        let (parts, _body) = req.into_parts();
217                                        let req = Request::from_parts(parts, Body::empty());
218                                        let version = req.version();
219                                        let method = req.method().to_owned();
220                                        let path = req.uri().path().to_owned();
221                                        let headers = req.headers().clone();
222                                        let history = Arc::clone(&history);
223                                        ServedRepository::handle_tuf_repo_request_infallible(
224                                            root.clone(),
225                                            Arc::clone(&response_overriders),
226                                            Arc::clone(&auto_response_creator),
227                                            req,
228                                        )
229                                        .inspect(move |x| {
230                                            history.lock().push(HistoryEntry {
231                                                    method: method.clone(),
232                                                    path: path.clone(),
233                                                    status: x.status(),
234                                                });
235                                            println!(
236                                                "{} [http repo] {:?} {} {} {:?} => {}",
237                                                Utc::now().format("%T.%6f"),
238                                                version,
239                                                method,
240                                                path,
241                                                headers,
242                                                x.status()
243                                            )
244                                        })
245                                        .map(Ok::<_, Infallible>)
246                                    });
247                                    let builder = hyper_util::server::conn::auto::Builder::new(fuchsia_hyper::Executor);
248                                    let conn = builder.serve_connection(hyper_util::rt::TokioIo::new(conn), service);
249                                    let mut conn = std::pin::pin!(conn);
250                                    futures::select! {
251                                        res = conn.as_mut().fuse() => {
252                                            if let Err(e) = res {
253                                                log::warn!("error serving repo connection: {e}");
254                                            }
255                                        }
256                                        _ = rx_stop.clone() => {
257                                            conn.as_mut().graceful_shutdown();
258                                            let _ = conn.await;
259                                        }
260                                    }
261                                }));
262                            }
263                            Some(Err(e)) => {
264                                log::error!("error accepting repo connection: {e}");
265                            }
266                            None => break,
267                        }
268                    }
269                    _ = rx_stop.clone() => break,
270                    _ = tasks.next() => {}
271                }
272            }
273            while tasks.next().await.is_some() {}
274        });
275
276        Ok(ServedRepository {
277            repo: self.repo,
278            stop,
279            server,
280            addr,
281            https_domain: self.https_domain,
282            auto_event_sender,
283            connection_attempts,
284            history,
285        })
286    }
287}
288
289fn parse_cert_chain(mut bytes: &[u8]) -> Vec<rustls::pki_types::CertificateDer<'static>> {
290    rustls_pemfile::certs(&mut bytes).collect::<Result<Vec<_>, _>>().expect("certs to parse")
291}
292
293fn parse_private_key(mut bytes: &[u8]) -> rustls::pki_types::PrivateKeyDer<'static> {
294    rustls_pemfile::private_key(&mut bytes)
295        .expect("private keys to parse")
296        .expect("one private key")
297}
298
299/// A [`Repository`] being served over HTTP.
300pub struct ServedRepository {
301    repo: Arc<Repository>,
302    stop: futures::channel::oneshot::Sender<()>,
303    server: Task<()>,
304    addr: SocketAddr,
305    auto_event_sender: EventSender,
306    connection_attempts: Arc<AtomicU64>,
307    https_domain: Option<Domain>,
308    history: Arc<Mutex<Vec<HistoryEntry>>>,
309}
310
311impl ServedRepository {
312    fn scheme(&self) -> &'static str {
313        if self.https_domain.is_some() { "https" } else { "http" }
314    }
315    /// Request the given path served by the repository over HTTP.
316    pub async fn get(&self, path: impl AsRef<str>) -> Result<Vec<u8>, Error> {
317        let url = format!("{}/{}", self.local_url(), path.as_ref());
318        get(url).await
319    }
320
321    /// Returns the URL that can be used to connect to this repository from this device.
322    pub fn local_url(&self) -> String {
323        format!("{}://localhost:{}", self.scheme(), self.addr.port())
324    }
325
326    /// Returns a sorted vector of all packages contained in this repository.
327    pub async fn list_packages(&self) -> Result<Vec<crate::repo::PackageEntry>, Error> {
328        let targets_json = self.get("targets.json").await?;
329        let mut packages = crate::repo::iter_packages(Cursor::new(targets_json))?
330            .collect::<Result<Vec<_>, _>>()?;
331        packages.sort_unstable();
332        Ok(packages)
333    }
334
335    /// Generates a [`MirrorConfigBuilder`] that points to this served repository.
336    pub fn get_mirror_config_builder(&self) -> MirrorConfigBuilder {
337        MirrorConfigBuilder::new(self.local_url().parse::<Uri>().unwrap()).unwrap()
338    }
339
340    /// Generates a [`MirrorConfig`] that points to this served repository.
341    fn get_mirror_config(&self, subscribe: bool) -> MirrorConfig {
342        self.get_mirror_config_builder().subscribe(subscribe).build()
343    }
344
345    /// Generate a [`RepositoryConfig`] suitable for configuring a package resolver to use this
346    /// served repository.
347    pub fn make_repo_config(&self, url: RepositoryUrl) -> RepositoryConfig {
348        self.repo.make_repo_config(url, Some(self.get_mirror_config(false)), false)
349    }
350
351    /// Generate a [`RepositoryConfig`] suitable for configuring a package resolver to use this
352    /// served repository. Set subscribe on the mirror configs to true.
353    pub fn make_repo_config_with_subscribe(&self, url: RepositoryUrl) -> RepositoryConfig {
354        self.repo.make_repo_config(url, Some(self.get_mirror_config(true)), false)
355    }
356
357    /// Generate a [`RepositoryConfig`] suitable for configuring a package resolver to use this
358    /// served repository with local mirroring enabled.
359    // TODO(https://fxbug.dev/42137938) delete this method once pkg-resolver can fetch metadata from a LocalMirror.
360    pub fn make_repo_config_with_local_mirror(&self, url: RepositoryUrl) -> RepositoryConfig {
361        self.repo.make_repo_config(url, Some(self.get_mirror_config(false)), true)
362    }
363
364    /// Generate a [`RepositoryConfig`] that permits persisting metadata.
365    pub fn make_repo_config_with_persistent_storage(&self, url: RepositoryUrl) -> RepositoryConfig {
366        self.repo
367            .make_repo_config_builder(url)
368            .add_mirror(self.get_mirror_config(false))
369            .use_local_mirror(false)
370            .repo_storage_type(RepositoryStorageType::Persistent)
371            .build()
372    }
373
374    /// Send an SSE event to all clients subscribed to /auto.
375    pub async fn send_auto_event(&self, event: &Event) {
376        self.auto_event_sender.send(event).await
377    }
378
379    /// Waits until `send_auto_event` would attempt to send an `Event` to exactly
380    /// `n` clients. Panics if extra clients are connected.
381    pub async fn wait_for_n_connected_auto_clients(&self, n: usize) {
382        loop {
383            let connected = self.auto_event_sender.client_count().await;
384            match connected.cmp(&n) {
385                std::cmp::Ordering::Equal => {
386                    break;
387                }
388                std::cmp::Ordering::Greater => {
389                    panic!("ServedRepository too many auto clients connected.");
390                }
391                _ => {}
392            }
393            fasync::Timer::new(Duration::from_millis(10)).await;
394        }
395    }
396
397    /// Errors all currently existing /auto `Response<Body>` streams.
398    pub async fn drop_all_auto_clients(&self) {
399        self.auto_event_sender.drop_all_clients().await
400    }
401
402    /// Gracefully signal the server to stop and returns a future that resolves when it terminates.
403    pub fn stop(self) -> impl Future<Output = ()> {
404        self.stop.send(()).expect("remote end to still be open");
405        self.server
406    }
407
408    /// Number of connection attempts.
409    pub fn connection_attempts(&self) -> u64 {
410        self.connection_attempts.load(Ordering::SeqCst)
411    }
412
413    /// The log of requests.
414    pub fn history(&self) -> Arc<Mutex<Vec<HistoryEntry>>> {
415        self.history.clone()
416    }
417
418    async fn handle_tuf_repo_request_infallible(
419        repo: PathBuf,
420        response_overriders: Arc<Vec<Arc<dyn HttpResponder>>>,
421        auto_response_creator: Arc<SseResponseCreator>,
422        req: Request<Body>,
423    ) -> Response<Body> {
424        let mut response = Self::handle_tuf_repo_request(repo, auto_response_creator, &req)
425            .await
426            .unwrap_or_else(|e| {
427                eprintln!("hyper tuf server error creating response for request {req:?}: {e:#}");
428                Response::builder()
429                    .status(StatusCode::INTERNAL_SERVER_ERROR)
430                    .body(Body::from("Error creating response".to_owned().into_bytes()))
431                    .unwrap()
432            });
433
434        for responder in response_overriders.iter() {
435            response = responder.respond(&req, response).await
436        }
437
438        response
439    }
440
441    async fn handle_tuf_repo_request(
442        repo: PathBuf,
443        auto_response_creator: Arc<SseResponseCreator>,
444        req: &Request<Body>,
445    ) -> Result<Response<Body>, Error> {
446        let fail =
447            |status: StatusCode| Response::builder().status(status).body(Body::empty()).unwrap();
448        if *req.method() != Method::GET {
449            return Ok(fail(StatusCode::NOT_FOUND));
450        } else if req.uri().query().is_some() {
451            return Ok(fail(StatusCode::BAD_REQUEST));
452        }
453
454        let uri_path = Path::new(req.uri().path());
455
456        // don't let queries escape the repo root.
457        if uri_path.components().any(|component| component == std::path::Component::ParentDir) {
458            return Ok(fail(StatusCode::NOT_FOUND));
459        }
460
461        let response = if uri_path.to_str() == Some("/auto") {
462            let resp = auto_response_creator.create().await;
463            let (parts, body) = resp.into_parts();
464            let body = body.filter_map(|res| async move {
465                match res {
466                    Ok(frame) => frame.into_data().ok().map(Ok),
467                    Err(e) => Some(Err(e)),
468                }
469            });
470            Response::from_parts(parts, Body::wrap_stream(body))
471        } else {
472            let fs_path = repo.join(uri_path.strip_prefix("/").unwrap_or(uri_path));
473            // TODO(https://fxbug.dev/42150717) synchronous IO in an async context.
474            let mut file = match std::fs::File::open(fs_path) {
475                Ok(file) => file,
476                Err(ref err) if err.kind() == std::io::ErrorKind::NotFound => {
477                    return Ok(Response::builder()
478                        .status(StatusCode::NOT_FOUND)
479                        .body(Body::from("File did not exist".to_owned().into_bytes()))
480                        .unwrap());
481                }
482                Err(e) => Err(e).context("opening file")?,
483            };
484
485            if let Some(range) = req.headers().get(http::header::RANGE) {
486                make_range_response(file, range).context("error making range response")?
487            } else {
488                let mut body = vec![];
489                file.read_to_end(&mut body).context("reading repo file")?;
490                Response::builder()
491                    .status(StatusCode::OK)
492                    .header(header::CONTENT_LENGTH, body.len())
493                    .body(Body::from(body))
494                    .unwrap()
495            }
496        };
497
498        Ok(response)
499    }
500}
501
502// TODO(https://fxbug.dev/42150593) use specific HTTP status codes for errors instead of mapping everything to
503// INTERNAL_SERVER_ERROR.
504fn make_range_response(
505    mut file: std::fs::File,
506    range: &http::HeaderValue,
507) -> Result<Response<Body>, Error> {
508    let HttpRange { first_byte_pos, last_byte_pos } =
509        range.try_into().context("parse range header")?;
510    let file_size = file.metadata().context("file metadata")?.len();
511    // TODO(https://fxbug.dev/42150593) return 416 if the range is invalid
512    file.seek(std::io::SeekFrom::Start(first_byte_pos)).context("seeking file")?;
513    let mut data = vec![0; 1 + last_byte_pos as usize - first_byte_pos as usize];
514    file.read_exact(&mut data).context("reading file for range request")?;
515
516    Ok(Response::builder()
517        .status(StatusCode::PARTIAL_CONTENT)
518        .header(header::CONTENT_LENGTH, data.len())
519        .header(
520            header::CONTENT_RANGE,
521            format!("bytes {first_byte_pos}-{last_byte_pos}/{file_size}"),
522        )
523        .body(Body::from(data))
524        .unwrap())
525}
526
527/// Parsed value of an HTTP request "Range" headers
528pub struct HttpRange {
529    first_byte_pos: u64,
530    last_byte_pos: u64,
531}
532
533impl HttpRange {
534    /// The first byte pos of the Range header.
535    pub fn first_byte_pos(&self) -> u64 {
536        self.first_byte_pos
537    }
538
539    /// The last byte pos of the Range header.
540    pub fn last_byte_pos(&self) -> u64 {
541        self.last_byte_pos
542    }
543}
544
545// TODO(https://fxbug.dev/42150593) use a spec compliant parser
546impl TryFrom<&http::HeaderValue> for HttpRange {
547    type Error = anyhow::Error;
548
549    fn try_from(range: &http::HeaderValue) -> Result<Self, Self::Error> {
550        let range = range.to_str().context("range header should be ascii")?;
551        let range = if let Some(range) = range.strip_prefix("bytes=") {
552            range
553        } else {
554            bail!("range header should start with 'bytes='");
555        };
556        let dash =
557            range.find('-').ok_or_else(|| anyhow::anyhow!("range header should have dash"))?;
558        let (first, last) = range.split_at(dash);
559        if last.len() < 2 {
560            bail!("range header last_byte_pos empty");
561        }
562        let first = first.parse().context("valid range first_byte_pos")?;
563        let last = last[1..].parse().context("valid range last_byte_pos")?;
564
565        if first > last {
566            bail!("first_byte_pos {} > last_byte_pos {}", first, last);
567        }
568
569        Ok(HttpRange { first_byte_pos: first, last_byte_pos: last })
570    }
571}
572
573async fn get(url: impl AsRef<str>) -> Result<Vec<u8>, Error> {
574    let request = Request::get(url.as_ref())
575        .body(http_body_util::Full::<hyper::body::Bytes>::new("".into()))
576        .map_err(Error::from)?;
577    let client = fuchsia_hyper::new_client();
578    let response = client.request(request).await?;
579
580    if response.status() != StatusCode::OK {
581        return Err(format_err!("unexpected status code: {:?}", response.status()));
582    }
583
584    let body = response.into_body().collect().await?.to_bytes();
585
586    Ok(body.to_vec())
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::package::PackageBuilder;
593    use crate::repo::RepositoryBuilder;
594    use assert_matches::assert_matches;
595
596    #[fuchsia::test]
597    #[ignore]
598    async fn test_serve_empty_hangs_on_last_get() {
599        let repo = Arc::new(RepositoryBuilder::new().build().await.unwrap());
600        let served_repo = repo.server().start().unwrap();
601
602        // contains no packages.
603        let packages = served_repo.list_packages().await.unwrap();
604        assert_eq!(packages, vec![]);
605
606        // no '..' allowed.
607        assert_matches!(served_repo.get("blobs/../root.json").await, Err(_));
608
609        // getting a known file fetches something.
610        let bytes = served_repo.get("targets.json").await.unwrap();
611        assert_ne!(bytes, Vec::<u8>::new());
612
613        // even if it doesn't go through the helper function.
614        let url = format!("{}/targets.json", served_repo.local_url());
615        let also_bytes = get(&url).await.unwrap();
616        assert_eq!(bytes, also_bytes);
617
618        // requests fail after stopping the server
619        served_repo.stop().await;
620
621        // FIXME(49247): this often flakes by hanging
622        assert_matches!(get(url).await, Err(_));
623    }
624
625    #[fuchsia::test]
626    async fn test_serve_empty() {
627        let repo = Arc::new(RepositoryBuilder::new().build().await.unwrap());
628        let served_repo = repo.server().start().unwrap();
629
630        // contains no packages.
631        let packages = served_repo.list_packages().await.unwrap();
632        assert_eq!(packages, vec![]);
633
634        // no '..' allowed.
635        assert_matches!(served_repo.get("blobs/../root.json").await, Err(_));
636
637        // getting a known file fetches something.
638        let bytes = served_repo.get("targets.json").await.unwrap();
639        assert_ne!(bytes, Vec::<u8>::new());
640
641        // even if it doesn't go through the helper function.
642        let url = format!("{}/targets.json", served_repo.local_url());
643        let also_bytes = get(&url).await.unwrap();
644        assert_eq!(bytes, also_bytes);
645    }
646
647    #[fuchsia::test]
648    async fn test_serve_packages() {
649        let same_contents = "same contents";
650        let repo = RepositoryBuilder::new()
651            .add_package(
652                PackageBuilder::new("rolldice")
653                    .add_resource_at("bin/rolldice", "#!/boot/bin/sh\necho 4\n".as_bytes())
654                    .add_resource_at(
655                        "meta/rolldice.cml",
656                        r#"{"program":{"binary":"bin/rolldice"}}"#.as_bytes(),
657                    )
658                    .add_resource_at("data/duplicate_a", "same contents".as_bytes())
659                    .build()
660                    .await
661                    .unwrap(),
662            )
663            .add_package(
664                PackageBuilder::new("fortune")
665                    .add_resource_at(
666                        "bin/fortune",
667                        "#!/boot/bin/sh\necho ask again later\n".as_bytes(),
668                    )
669                    .add_resource_at(
670                        "meta/fortune.cml",
671                        r#"{"program":{"binary":"bin/fortune"}}"#.as_bytes(),
672                    )
673                    .add_resource_at("data/duplicate_b", same_contents.as_bytes())
674                    .add_resource_at("data/duplicate_c", same_contents.as_bytes())
675                    .build()
676                    .await
677                    .unwrap(),
678            )
679            .build()
680            .await
681            .unwrap();
682        let repo = Arc::new(repo);
683
684        let local_packages = repo.list_packages().unwrap();
685
686        let served_repository = repo.server().start().unwrap();
687        let served_packages = served_repository.list_packages().await.unwrap();
688        assert_eq!(local_packages, served_packages);
689    }
690}