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