1#![allow(clippy::let_unit_value)]
6
7use anyhow::{Context, Error};
8use async_trait::async_trait;
9use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, Proxy, ServerEnd};
10use fidl_fuchsia_io::DirectoryProxy;
11use fidl_fuchsia_paver::PaverRequestStream;
12use fidl_fuchsia_pkg_ext::RepositoryConfigs;
13use fuchsia_component::server::ServiceFs;
14use fuchsia_component_test::LocalComponentHandles;
15use fuchsia_merkle::Hash;
16use fuchsia_pkg_testing::serve::ServedRepository;
17use fuchsia_pkg_testing::{Package, RepositoryBuilder};
18use fuchsia_sync::Mutex;
19use futures::prelude::*;
20use isolated_ota::{OmahaConfig, UpdateUrlSource};
21use mock_omaha_server::{
22 OmahaResponse, OmahaServer, OmahaServerBuilder, ResponseAndMetadata, ResponseMap,
23};
24use mock_paver::{MockPaverService, MockPaverServiceBuilder};
25use std::collections::{BTreeMap, BTreeSet};
26use std::io::Write;
27use std::str::FromStr;
28use std::sync::Arc;
29use tempfile::TempDir;
30use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
31
32pub const GLOBAL_SSL_CERTS_PATH: &str = "/config/ssl";
33const EMPTY_REPO_PATH: &str = "/pkg/empty-repo";
34const TEST_CERTS_PATH: &str = "/pkg/data/ssl";
35const TEST_REPO_URL: &str = "fuchsia-pkg://integration.test.fuchsia.com";
36
37pub enum OmahaState {
38 Disabled(Option<url::Url>),
40 Auto(OmahaResponse),
42 Manual(OmahaConfig),
44}
45
46pub struct TestParams {
47 pub blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
48 pub board: String,
49 pub channel: String,
50 pub expected_blobfs_contents: BTreeSet<Hash>,
51 pub paver: Arc<MockPaverService>,
52 pub repo_config_dir: TempDir,
53 pub ssl_certs: DirectoryProxy,
54 pub update_merkle: Hash,
55 pub version: String,
56 pub update_url_source: UpdateUrlSource,
57 pub paver_connector: ClientEnd<fio::DirectoryMarker>,
58}
59
60pub async fn expose_mock_paver(
66 handles: LocalComponentHandles,
67 paver_dir_proxy: fio::DirectoryProxy,
68) -> Result<(), Error> {
69 let mut fs = ServiceFs::new();
70
71 fs.dir("svc").add_service_connector(
72 move |server_end: ServerEnd<fidl_fuchsia_paver::PaverMarker>| {
73 fdio::service_connect_at(
74 paver_dir_proxy.as_channel().as_ref(),
75 &format!("/{}", fidl_fuchsia_paver::PaverMarker::PROTOCOL_NAME),
76 server_end.into_channel(),
77 )
78 .expect("failed to connect to paver service node");
79 },
80 );
81
82 fs.serve_connection(handles.outgoing_dir).expect("failed to serve paver fs connection");
83 fs.collect::<()>().await;
84 Ok(())
85}
86
87#[async_trait(?Send)]
88pub trait TestExecutor<R> {
89 async fn run(&self, params: TestParams) -> R;
90}
91
92pub struct TestEnvBuilder<R> {
93 blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
94 board: String,
95 channel: String,
96 omaha: OmahaState,
97 packages: Vec<Package>,
98 paver: MockPaverServiceBuilder,
99 repo_config: Option<RepositoryConfigs>,
100 version: String,
101 test_executor: Option<Box<dyn TestExecutor<R>>>,
102 fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
104 recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
106 firmware_images: BTreeMap<String, Vec<u8>>,
107}
108
109impl<R> TestEnvBuilder<R> {
110 #[allow(clippy::new_without_default)]
111 pub fn new() -> Self {
112 TestEnvBuilder {
113 blobfs: None,
114 board: "test-board".to_owned(),
115 channel: "test".to_owned(),
116 omaha: OmahaState::Disabled(Some(format!("{TEST_REPO_URL}/update").parse().unwrap())),
117 packages: vec![],
118 paver: MockPaverServiceBuilder::new(),
119 repo_config: None,
120 version: "0.1.2.3".to_owned(),
121 test_executor: None,
122 fuchsia_image: None,
123 recovery_image: None,
124 firmware_images: BTreeMap::new(),
125 }
126 }
127
128 pub fn add_package(mut self, pkg: Package) -> Self {
132 self.packages.push(pkg);
133 self
134 }
135
136 pub fn blobfs(mut self, client: ClientEnd<fio::DirectoryMarker>) -> Self {
137 self.blobfs = Some(client);
138 self
139 }
140
141 pub fn repo_config(mut self, repo: RepositoryConfigs) -> Self {
144 self.repo_config = Some(repo);
145 self
146 }
147
148 pub fn omaha_state(mut self, state: OmahaState) -> Self {
151 self.omaha = state;
152 self
153 }
154
155 pub fn paver<F>(mut self, func: F) -> Self
157 where
158 F: FnOnce(MockPaverServiceBuilder) -> MockPaverServiceBuilder,
159 {
160 self.paver = func(self.paver);
161 self
162 }
163
164 pub fn test_executor(mut self, executor: Box<dyn TestExecutor<R>>) -> Self {
165 self.test_executor = Some(executor);
166 self
167 }
168
169 pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
171 assert_eq!(self.fuchsia_image, None);
172 self.fuchsia_image = Some((zbi, vbmeta));
173 self
174 }
175
176 pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
178 assert_eq!(self.recovery_image, None);
179 self.recovery_image = Some((zbi, vbmeta));
180 self
181 }
182
183 pub fn firmware_image(mut self, type_: String, content: Vec<u8>) -> Self {
185 assert_eq!(self.firmware_images.insert(type_, content), None);
186 self
187 }
188
189 pub async fn build(mut self) -> Result<TestEnv<R>, Error> {
191 let (repo_config, served_repo, ssl_certs, expected_blobfs_contents, merkle) =
192 if let Some(repo_config) = self.repo_config {
193 (
196 repo_config,
197 None,
198 fuchsia_fs::directory::open_in_namespace(
199 GLOBAL_SSL_CERTS_PATH,
200 fio::PERM_READABLE,
201 )
202 .unwrap(),
203 BTreeSet::new(),
204 Hash::from_str(
205 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
206 )
207 .expect("make merkle"),
208 )
209 } else {
210 let mut update =
213 fuchsia_pkg_testing::UpdatePackageBuilder::new(TEST_REPO_URL.parse().unwrap())
214 .packages(
215 self.packages
216 .iter()
217 .map(|p| {
218 fuchsia_url::PinnedAbsolutePackageUrl::new(
219 TEST_REPO_URL.parse().unwrap(),
220 p.name().clone(),
221 None,
222 *p.hash(),
223 )
224 })
225 .collect::<Vec<_>>(),
226 )
227 .firmware_images(self.firmware_images);
228 if let Some((zbi, vbmeta)) = self.fuchsia_image {
229 update = update.fuchsia_image(zbi, vbmeta);
230 }
231 if let Some((zbi, vbmeta)) = self.recovery_image {
232 update = update.recovery_image(zbi, vbmeta);
233 }
234 let (update, images) = update.build().await;
235
236 let expected_blobfs_contents = self
238 .packages
239 .iter()
240 .chain([update.as_package()])
241 .flat_map(|p| p.list_blobs())
242 .collect();
243
244 let repo = Arc::new(
245 self.packages
246 .iter()
247 .chain([update.as_package(), &images])
248 .fold(
249 RepositoryBuilder::from_template_dir(EMPTY_REPO_PATH)
250 .add_package(update.as_package()),
251 |repo, package| repo.add_package(package),
252 )
253 .build()
254 .await
255 .expect("build repo"),
256 );
257
258 let served_repo = Arc::clone(&repo).server().start().expect("serve repo");
259 let config = RepositoryConfigs::Version1(vec![
260 served_repo.make_repo_config(TEST_REPO_URL.parse().expect("make repo config")),
261 ]);
262
263 let update_merkle = *update.as_package().hash();
264 let mut packages = vec![update.into_package()];
267 packages.append(&mut self.packages);
268 (
269 config,
270 Some(served_repo),
271 fuchsia_fs::directory::open_in_namespace(TEST_CERTS_PATH, fio::PERM_READABLE)
272 .unwrap(),
273 expected_blobfs_contents,
274 update_merkle,
275 )
276 };
277
278 let dir = tempfile::tempdir()?;
279 let mut path = dir.path().to_owned();
280 path.push("repo_config.json");
281 let path = path.as_path();
282 let mut file =
283 std::io::BufWriter::new(std::fs::File::create(path).context("creating file")?);
284 serde_json::to_writer(&mut file, &repo_config).unwrap();
285 file.flush().unwrap();
286
287 Ok(TestEnv {
288 blobfs: self.blobfs,
289 board: self.board,
290 channel: self.channel,
291 omaha: self.omaha,
292 expected_blobfs_contents,
293 paver: Arc::new(self.paver.build()),
294 _repo: served_repo,
295 repo_config_dir: dir,
296 ssl_certs,
297 update_merkle: merkle,
298 version: self.version,
299 test_executor: self.test_executor.expect("test executor must be set"),
300 })
301 }
302}
303
304pub struct TestEnv<R> {
305 blobfs: Option<ClientEnd<fio::DirectoryMarker>>,
306 channel: String,
307 omaha: OmahaState,
308 expected_blobfs_contents: BTreeSet<Hash>,
309 paver: Arc<MockPaverService>,
310 _repo: Option<ServedRepository>,
311 repo_config_dir: tempfile::TempDir,
312 ssl_certs: DirectoryProxy,
313 update_merkle: Hash,
314 board: String,
315 version: String,
316 test_executor: Box<dyn TestExecutor<R>>,
317}
318
319impl<R> TestEnv<R> {
320 async fn start_omaha(omaha: OmahaState, merkle: Hash) -> Result<UpdateUrlSource, Error> {
321 match omaha {
322 OmahaState::Disabled(url) => Ok(match url {
323 Some(url) => UpdateUrlSource::UpdateUrl(url),
324 None => UpdateUrlSource::UseDefault,
325 }),
326 OmahaState::Manual(cfg) => Ok(UpdateUrlSource::OmahaConfig(cfg)),
327 OmahaState::Auto(response) => {
328 let mut response = ResponseAndMetadata { response, ..Default::default() };
330 let p: Vec<_> = response.package_name.split("?hash=").collect();
331 assert_eq!(p.len(), 2);
332 response.package_name = format!("{}?hash={}", p[0], merkle);
333 let server = OmahaServerBuilder::default()
334 .responses_by_appid(
335 vec![("integration-test-appid".to_string(), response)]
336 .into_iter()
337 .collect::<ResponseMap>(),
338 )
339 .build()
340 .unwrap();
341 let addr = OmahaServer::start_and_detach(Arc::new(Mutex::new(server)), None)
342 .await
343 .context("Starting omaha server")?;
344 let config =
345 OmahaConfig { app_id: "integration-test-appid".to_owned(), server_url: addr };
346
347 Ok(UpdateUrlSource::OmahaConfig(config))
348 }
349 }
350 }
351
352 pub async fn run(self) -> R {
354 let update_url_source = TestEnv::<R>::start_omaha(self.omaha, self.update_merkle)
355 .await
356 .expect("Starting Omaha server");
357
358 let mut service_fs = ServiceFs::new();
359 let paver_clone = Arc::clone(&self.paver);
360 service_fs.add_fidl_service(move |stream: PaverRequestStream| {
361 fasync::Task::spawn(
362 Arc::clone(&paver_clone)
363 .run_paver_service(stream)
364 .unwrap_or_else(|e| panic!("Failed to run mock paver: {e:?}")),
365 )
366 .detach();
367 });
368
369 let (client, server) =
370 fidl::endpoints::create_endpoints::<fidl_fuchsia_io::DirectoryMarker>();
371 service_fs
372 .serve_connection(server.into_channel().into())
373 .expect("Failed to serve connection");
374 fasync::Task::spawn(service_fs.collect()).detach();
375
376 let params = TestParams {
377 blobfs: self.blobfs,
378 board: self.board,
379 channel: self.channel,
380 expected_blobfs_contents: self.expected_blobfs_contents,
381 paver: self.paver,
382 repo_config_dir: self.repo_config_dir,
383 ssl_certs: self.ssl_certs,
384 update_merkle: self.update_merkle,
385 version: self.version,
386 update_url_source,
387 paver_connector: client,
388 };
389
390 self.test_executor.run(params).await
391 }
392}