blob: 66fc5cb68a670e0dae8c62ff619cc720a5318a40 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::{
app_set::{EagerPackage, FuchsiaAppSet},
channel::{ChannelConfig, ChannelConfigs},
eager_package_config::{EagerPackageConfig, EagerPackageConfigs},
};
use anyhow::{anyhow, Error};
use fidl_fuchsia_boot::{ArgumentsMarker, ArgumentsProxy};
use fidl_fuchsia_pkg::{self as fpkg, CupMarker, CupProxy, GetInfoError};
use log::{error, info, warn};
use omaha_client::{
common::App,
configuration::{Config, Updater},
protocol::{request::OS, Cohort},
};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::iter::FromIterator;
use version::Version;
// TODO: This is not 0.0.0.0 because that would cause state machine to not start. We should find a
// better way to achieve that when build version is invalid.
const MINIMUM_VALID_VERSION: [u32; 4] = [0, 0, 0, 1];
/// This struct is the overall "configuration" of the omaha client. Minus the PolicyConfig. That
/// should probably be included in here as well, eventually.
pub struct ClientConfiguration {
pub platform_config: omaha_client::configuration::Config,
pub app_set: FuchsiaAppSet,
pub channel_data: ChannelData,
}
/// This wraps up all the channel-related configuration (name, appid, config, and any other params
/// that they might have). Mostly as a grouping for returning from fns in a cleaner way than a
/// tuple provides.
pub struct ChannelData {
pub source: ChannelSource,
pub name: Option<String>,
pub config: Option<ChannelConfig>,
pub appid: String,
}
/// The source of the channel configuration.
#[derive(Debug, Eq, PartialEq)]
pub enum ChannelSource {
MinFS,
Default,
VbMeta,
}
impl ClientConfiguration {
/// Given an (optional) set of ChannelConfigs, load all the other configs that are needed, and
/// construct an overall configuration struct that can then be used.
/// TODO: Move the reading of channel_configs.json into this.
pub async fn initialize(channel_configs: Option<&ChannelConfigs>) -> Result<Self, io::Error> {
let version = get_version()?;
let vbmeta_data = VbMetaData::from_namespace().await.unwrap_or_else(|e| {
warn!("Failed to get data from vbmeta {:?}", e);
VbMetaData::default()
});
Ok(Self::initialize_from(&version, channel_configs, vbmeta_data).await)
}
async fn initialize_from(
version: &str,
channel_configs: Option<&ChannelConfigs>,
VbMetaData { appid, channel_name, realm_id, product_id, service_url }: VbMetaData,
) -> Self {
let (channel_name, channel_source) = if channel_name.is_some() {
(channel_name, ChannelSource::VbMeta)
} else {
// The channel wasn't found in VBMeta, so instead look for a default channel in the
// channel configuration.
if let Some(ChannelConfigs { default_channel: Some(name), .. }) = channel_configs {
(Some(name.clone()), ChannelSource::Default)
} else {
// Channel will be loaded from `Storage` by state machine.
(None, ChannelSource::MinFS)
}
};
// Locate the channel config for the channel, if it exists.
let channel_config = if let (Some(name), Some(configs)) = (&channel_name, channel_configs) {
configs.get_channel(name)
} else {
None
};
// If no appid in vbmeta, look up the appid of the channel from the channel config.
let appid = if let (None, Some(config)) = (&appid, &channel_config) {
config.appid.clone()
} else {
appid
};
// If no appid in the channel configs, then attempt to read from config data.
let appid =
appid.unwrap_or_else(|| match fs::read_to_string("/config/data/omaha_app_id") {
Ok(id) => id,
Err(e) => {
error!("Unable to read omaha app id from config/data: {:?}", e);
String::new()
}
});
// Construct the Fuchsia system app.
let mut extra_fields: Vec<(String, String)> =
vec![("channel".to_string(), channel_name.clone().unwrap_or_default())];
if let Some(product_id) = product_id {
extra_fields.push(("product_id".to_string(), product_id));
}
if let Some(realm_id) = realm_id {
extra_fields.push(("realm_id".to_string(), realm_id));
}
let app = App::builder()
.id(appid.clone())
.version(Self::parse_version(version))
.cohort(Cohort {
hint: channel_name.clone(),
name: channel_name.clone(),
..Cohort::default()
})
.extra_fields(HashMap::from_iter(extra_fields.into_iter()))
.build();
let mut app_set = FuchsiaAppSet::new(app);
let mut platform_config = get_config(&version, service_url);
match EagerPackageConfigs::from_namespace() {
Ok(eager_package_configs) => {
let proxy = fuchsia_component::client::connect_to_protocol::<CupMarker>()
.map_err(|e| error!("Failed to connect to Cup protocol {:#}", anyhow!(e)))
.ok();
Self::add_eager_packages(
&mut app_set,
&mut platform_config,
eager_package_configs,
proxy,
)
.await
}
Err(e) => {
match e.downcast_ref::<std::io::Error>() {
Some(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => {
warn!("eager package config not found: {:#}", anyhow!(e))
}
_ => error!(
"Failed to load eager package config from namespace: {:#}",
anyhow!(e)
),
};
}
}
ClientConfiguration {
platform_config,
app_set,
channel_data: ChannelData {
source: channel_source,
name: channel_name,
config: channel_config,
appid,
},
}
}
/// Add all eager packages in eager package config to app set.
/// Also adds Omaha config to platform_config.
async fn add_eager_packages(
app_set: &mut FuchsiaAppSet,
platform_config: &mut Config,
eager_package_configs: EagerPackageConfigs,
cup: Option<CupProxy>,
) {
if let Some(server) = eager_package_configs.server {
platform_config.service_url = server.service_url;
platform_config.omaha_public_keys = Some(server.public_keys);
}
for package in eager_package_configs.packages {
let (version, channel_config) =
Self::get_eager_package_version_and_channel(&package, &cup).await;
let appid = match channel_config.as_ref().and_then(|c| c.appid.as_ref()) {
Some(appid) => &appid,
None => {
error!("no appid for package '{}'", package.url);
""
}
};
let app_builder = App::builder().id(appid).version(version);
let app = if let Some(channel_config) = channel_config {
app_builder
.cohort(Cohort {
hint: Some(channel_config.name.clone()),
name: Some(channel_config.name.clone()),
..Cohort::default()
})
.extra_fields([("channel".to_string(), channel_config.name.clone())])
.build()
} else {
app_builder.build()
};
app_set.add_eager_package(EagerPackage::new(app, Some(package.channel_config)));
}
}
async fn get_eager_package_version_and_channel(
package: &EagerPackageConfig,
cup: &Option<CupProxy>,
) -> (Version, Option<ChannelConfig>) {
let default_version = Version::from(MINIMUM_VALID_VERSION);
if let Some(ref cup) = cup {
match cup.get_info(&mut fpkg::PackageUrl { url: package.url.to_string() }).await {
Ok(Ok((cup_version, cup_channel))) => {
let channel_config =
package.channel_config.get_channel(&cup_channel).or_else(|| {
error!(
"'{}' channel from CUP for package '{}' is not a known channel",
cup_channel, package.url
);
package.channel_config.get_default_channel()
});
let version = cup_version.parse().unwrap_or_else(|e| {
error!(
"Unable to parse '{}' as Omaha version format: {:?}",
cup_version, e
);
default_version
});
return (version, channel_config);
}
Ok(Err(GetInfoError::NotAvailable)) => {
info!("Eager package '{}' not currently available on the device", package.url);
}
Ok(Err(e)) => {
error!(
"Failed to get info about eager package '{}' from CUP: {:?}",
package.url, e
);
}
Err(e) => {
error!("Failed to send request to fuchsia.pkg.Cup: {:#}", anyhow!(e));
}
}
}
(default_version, package.channel_config.get_default_channel())
}
/// Helper to wrap the parsing of a version string, logging any parse errors and making sure
/// that there's still some valid value as a result.
fn parse_version(version: &str) -> Version {
match version.parse::<Version>() {
Ok(parsed_version) => parsed_version,
Err(e) => {
error!("Unable to parse '{}' as Omaha version format: {:?}", version, e);
Version::from([0])
}
}
}
}
pub fn get_config(version: &str, vbmeta_service_url: Option<String>) -> Config {
// This file does not exist in production, it is only used in integration/e2e testing.
let service_url = vbmeta_service_url.unwrap_or_else(|| {
fs::read_to_string("/config/data/omaha_url").unwrap_or_else(|_| {
"https://clients2.google.com/service/update2/fuchsia/json".to_string()
})
});
Config {
updater: Updater { name: "Fuchsia".to_string(), version: Version::from([0, 0, 1, 0]) },
os: OS {
platform: "Fuchsia".to_string(),
version: version.to_string(),
service_pack: "".to_string(),
arch: std::env::consts::ARCH.to_string(),
},
service_url,
omaha_public_keys: None,
}
}
pub fn get_version() -> Result<String, io::Error> {
fs::read_to_string("/config/build-info/version").map(|s| s.trim_end().to_string())
}
#[derive(Debug, Default, Eq, PartialEq)]
struct VbMetaData {
appid: Option<String>,
channel_name: Option<String>,
realm_id: Option<String>,
product_id: Option<String>,
service_url: Option<String>,
}
impl VbMetaData {
async fn from_namespace() -> Result<Self, Error> {
let proxy = fuchsia_component::client::connect_to_protocol::<ArgumentsMarker>()?;
Self::from_proxy(proxy).await
}
async fn from_proxy(proxy: ArgumentsProxy) -> Result<Self, Error> {
let keys = vec!["omaha_app_id", "ota_channel", "ota_realm", "product_id", "omaha_url"];
let mut res = proxy.get_strings(&mut keys.into_iter()).await?;
if res.len() != 5 {
Err(anyhow!("Remote endpoint returned {} values, expected 5", res.len()))
} else {
Ok(Self {
appid: res[0].take(),
channel_name: res[1].take(),
realm_id: res[2].take(),
product_id: res[3].take(),
service_url: res[4].take(),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eager_package_config::{EagerPackageConfig, OmahaServer};
use fidl::endpoints::create_proxy_and_stream;
use fidl_fuchsia_boot::ArgumentsRequest;
use fidl_fuchsia_pkg::CupRequest;
use fuchsia_async as fasync;
use fuchsia_url::UnpinnedAbsolutePackageUrl;
use futures::prelude::*;
use omaha_client::{
app_set::AppSet,
cup_ecdsa::{test_support::make_default_public_key_for_test, PublicKeyAndId, PublicKeys},
};
use std::{collections::HashMap, convert::TryInto};
#[fasync::run_singlethreaded(test)]
async fn test_get_config() {
let client_config =
ClientConfiguration::initialize_from("1.2.3.4", None, VbMetaData::default()).await;
let config = client_config.platform_config;
assert_eq!(config.updater.name, "Fuchsia");
let os = config.os;
assert_eq!(os.platform, "Fuchsia");
assert_eq!(os.version, "1.2.3.4");
assert_eq!(os.arch, std::env::consts::ARCH);
assert_eq!(config.service_url, "https://clients2.google.com/service/update2/fuchsia/json");
}
#[fasync::run_singlethreaded(test)]
async fn test_get_app_set_config_read_init() {
let config =
ClientConfiguration::initialize_from("1.2.3.4", None, VbMetaData::default()).await;
assert_eq!(config.channel_data.source, ChannelSource::MinFS);
let apps = config.app_set.get_apps();
assert_eq!(apps.len(), 1);
assert_eq!(apps[0].id, "fuchsia:test-app-id");
assert_eq!(apps[0].version, Version::from([1, 2, 3, 4]));
assert_eq!(apps[0].cohort.name, None);
assert_eq!(apps[0].cohort.hint, None);
assert_eq!(apps[0].extra_fields.get("product_id"), None);
assert_eq!(apps[0].extra_fields.get("realm_id"), None);
}
#[fasync::run_singlethreaded(test)]
async fn test_get_app_set_default_channel() {
let config = ClientConfiguration::initialize_from(
"1.2.3.4",
Some(&ChannelConfigs {
default_channel: Some("default-channel".to_string()),
known_channels: vec![],
}),
VbMetaData::default(),
)
.await;
assert_eq!(config.channel_data.source, ChannelSource::Default);
let apps = config.app_set.get_apps();
assert_eq!(apps.len(), 1);
assert_eq!(apps[0].id, "fuchsia:test-app-id");
assert_eq!(apps[0].version, Version::from([1, 2, 3, 4]));
assert_eq!(apps[0].cohort.name, Some("default-channel".to_string()));
assert_eq!(apps[0].cohort.hint, Some("default-channel".to_string()));
}
#[fasync::run_singlethreaded(test)]
async fn test_channel_data_configured() {
let channel_config = ChannelConfig::with_appid("some-channel", "some-appid");
let channel_configs = ChannelConfigs {
default_channel: Some(channel_config.name.clone()),
known_channels: vec![channel_config.clone()],
};
let config = ClientConfiguration::initialize_from(
"1.2.3.4",
Some(&channel_configs),
VbMetaData::default(),
)
.await;
let channel_data = config.channel_data;
assert_eq!(channel_data.source, ChannelSource::Default);
assert_eq!(channel_data.name, Some("some-channel".to_string()));
assert_eq!(channel_data.config, Some(channel_config));
assert_eq!(channel_data.appid, "some-appid");
}
#[fasync::run_singlethreaded(test)]
async fn test_get_app_set_appid_from_channel_configs() {
let config = ClientConfiguration::initialize_from(
"1.2.3.4",
Some(&ChannelConfigs {
default_channel: Some("some-channel".to_string()),
known_channels: vec![
ChannelConfig::new("no-appid-channel"),
ChannelConfig::with_appid("wrong-channel", "wrong-appid"),
ChannelConfig::with_appid("some-channel", "some-appid"),
ChannelConfig::with_appid("some-other-channel", "some-other-appid"),
],
}),
VbMetaData::default(),
)
.await;
assert_eq!(config.channel_data.source, ChannelSource::Default);
let apps = config.app_set.get_apps();
assert_eq!(apps.len(), 1);
assert_eq!(apps[0].id, "some-appid");
assert_eq!(apps[0].version, Version::from([1, 2, 3, 4]));
assert_eq!(apps[0].cohort.name, Some("some-channel".to_string()));
assert_eq!(apps[0].cohort.hint, Some("some-channel".to_string()));
}
#[fasync::run_singlethreaded(test)]
async fn test_get_app_set_invalid_version() {
let config =
ClientConfiguration::initialize_from("invalid version", None, VbMetaData::default())
.await;
let apps = config.app_set.get_apps();
assert_eq!(apps[0].version, Version::from([0]));
}
#[fasync::run_singlethreaded(test)]
async fn test_get_init_from_vbmeta() {
let config = ClientConfiguration::initialize_from(
"1.2.3.4",
Some(&ChannelConfigs {
default_channel: Some("wrong-channel".to_string()),
known_channels: vec![ChannelConfig::with_appid("wrong-channel", "wrong-appid")],
}),
VbMetaData {
appid: Some("vbmeta-appid".to_string()),
channel_name: Some("vbmeta-channel".to_string()),
realm_id: Some("vbmeta-realm".to_string()),
product_id: Some("vbmeta-product".to_string()),
service_url: Some("vbmeta-url".to_string()),
},
)
.await;
assert_eq!(config.channel_data.source, ChannelSource::VbMeta);
assert_eq!(config.channel_data.name, Some("vbmeta-channel".to_string()));
assert_eq!(config.channel_data.config, None);
assert_eq!(config.channel_data.appid, "vbmeta-appid");
let apps = config.app_set.get_apps();
assert_eq!(apps.len(), 1);
assert_eq!(apps[0].id, "vbmeta-appid");
assert_eq!(apps[0].version, Version::from([1, 2, 3, 4]));
assert_eq!(apps[0].cohort.name, Some("vbmeta-channel".to_string()));
assert_eq!(apps[0].cohort.hint, Some("vbmeta-channel".to_string()));
assert_eq!(
apps[0].extra_fields,
HashMap::from([
("channel".to_string(), "vbmeta-channel".to_string()),
("product_id".to_string(), "vbmeta-product".to_string()),
("realm_id".to_string(), "vbmeta-realm".to_string())
])
);
}
#[fasync::run_singlethreaded(test)]
async fn test_get_data_from_vbmeta() {
let (proxy, mut stream) = create_proxy_and_stream::<ArgumentsMarker>().unwrap();
let fut = async move {
assert_eq!(
VbMetaData::from_proxy(proxy).await.unwrap(),
VbMetaData {
appid: Some("test-appid".to_string()),
channel_name: Some("test-channel".to_string()),
realm_id: Some("test-realm".to_string()),
product_id: Some("test-product".to_string()),
service_url: Some("test-url".to_string())
}
);
};
let stream_fut = async move {
match stream.next().await.unwrap() {
Ok(ArgumentsRequest::GetStrings { keys, responder }) => {
assert_eq!(
keys,
vec!["omaha_app_id", "ota_channel", "ota_realm", "product_id", "omaha_url"]
);
let vec: Vec<Option<&str>> = vec![
Some("test-appid"),
Some("test-channel"),
Some("test-realm"),
Some("test-product"),
Some("test-url"),
];
responder.send(&mut vec.into_iter()).expect("send failed");
}
request => panic!("Unexpected request: {:?}", request),
}
};
future::join(fut, stream_fut).await;
}
#[fasync::run_singlethreaded(test)]
async fn test_get_data_from_vbmeta_missing() {
let (proxy, mut stream) = create_proxy_and_stream::<ArgumentsMarker>().unwrap();
let fut = async move {
let vbmeta_data = VbMetaData::from_proxy(proxy).await.unwrap();
assert_eq!(vbmeta_data, VbMetaData::default());
};
let stream_fut = async move {
match stream.next().await.unwrap() {
Ok(ArgumentsRequest::GetStrings { keys, responder }) => {
assert_eq!(keys.len(), 5);
let ret: Vec<Option<&str>> = vec![None; 5];
responder.send(&mut ret.into_iter()).expect("send failed");
}
request => panic!("Unexpected request: {:?}", request),
}
};
future::join(fut, stream_fut).await;
}
#[fasync::run_singlethreaded(test)]
async fn test_get_data_from_vbmeta_error() {
let (proxy, mut stream) = create_proxy_and_stream::<ArgumentsMarker>().unwrap();
let fut = async move {
assert!(VbMetaData::from_proxy(proxy).await.is_err());
};
let stream_fut = async move {
match stream.next().await.unwrap() {
Ok(ArgumentsRequest::GetStrings { .. }) => {
// Don't respond.
}
request => panic!("Unexpected request: {:?}", request),
}
};
future::join(fut, stream_fut).await;
}
#[fasync::run_singlethreaded(test)]
async fn test_add_eager_packages() {
let mut platform_config = get_config("1.0.0.0", None);
let system_app = App::builder().id("system_app_id").version([1]).build();
let mut app_set = FuchsiaAppSet::new(system_app.clone());
let public_keys = PublicKeys {
latest: PublicKeyAndId {
id: 123.try_into().unwrap(),
key: make_default_public_key_for_test(),
},
historical: vec![],
};
assert!(platform_config.omaha_public_keys.is_none());
let config = EagerPackageConfigs {
server: Some(OmahaServer {
service_url: "https://example.com".into(),
public_keys: public_keys.clone(),
}),
packages: vec![
EagerPackageConfig {
url: UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.com/package")
.unwrap(),
flavor: Some("debug".into()),
channel_config: ChannelConfigs {
default_channel: Some("stable".into()),
known_channels: vec![
ChannelConfig {
name: "stable".into(),
repo: "stable".into(),
appid: Some("1a2b3c4d".into()),
check_interval_secs: None,
},
ChannelConfig {
name: "beta".into(),
repo: "beta".into(),
appid: Some("1a2b3c4d".into()),
check_interval_secs: None,
},
ChannelConfig {
name: "alpha".into(),
repo: "alpha".into(),
appid: Some("1a2b3c4d".into()),
check_interval_secs: None,
},
ChannelConfig {
name: "test".into(),
repo: "test".into(),
appid: Some("2b3c4d5e".into()),
check_interval_secs: None,
},
],
},
},
EagerPackageConfig {
url: UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.com/package2")
.unwrap(),
flavor: None,
channel_config: ChannelConfigs {
default_channel: None,
known_channels: vec![ChannelConfig {
name: "stable".into(),
repo: "stable".into(),
appid: Some("3c4d5e6f".into()),
check_interval_secs: None,
}],
},
},
],
};
// without CUP
ClientConfiguration::add_eager_packages(
&mut app_set,
&mut platform_config,
config.clone(),
None,
)
.await;
assert_eq!(platform_config.omaha_public_keys, Some(public_keys));
let package_app = App::builder()
.id("1a2b3c4d")
.version(MINIMUM_VALID_VERSION)
.cohort(Cohort {
hint: Some("stable".into()),
name: Some("stable".into()),
..Cohort::default()
})
.extra_fields([("channel".to_string(), "stable".to_string())])
.build();
let package2_app = App::builder().id("").version(MINIMUM_VALID_VERSION).build();
assert_eq!(app_set.get_apps(), vec![system_app.clone(), package_app, package2_app]);
// now with CUP
let mut app_set = FuchsiaAppSet::new(system_app.clone());
let (proxy, mut stream) = create_proxy_and_stream::<CupMarker>().unwrap();
let stream_fut = async move {
while let Some(request) = stream.next().await {
match request {
Ok(CupRequest::GetInfo { url, responder }) => {
let response = match url.url.as_str() {
"fuchsia-pkg://example.com/package" => ("1.2.3".into(), "beta".into()),
"fuchsia-pkg://example.com/package2" => {
("4.5.6".into(), "stable".into())
}
url => panic!("unexpected url {}", url),
};
responder.send(&mut Ok(response)).unwrap();
}
request => panic!("Unexpected request: {:?}", request),
}
}
};
let fut = ClientConfiguration::add_eager_packages(
&mut app_set,
&mut platform_config,
config,
Some(proxy),
);
future::join(fut, stream_fut).await;
let package_app = App::builder()
.id("1a2b3c4d")
.version([1, 2, 3, 0])
.cohort(Cohort {
hint: Some("beta".into()),
name: Some("beta".into()),
..Cohort::default()
})
.extra_fields([("channel".to_string(), "beta".to_string())])
.build();
let package2_app = App::builder()
.id("3c4d5e6f")
.version([4, 5, 6, 0])
.cohort(Cohort {
hint: Some("stable".into()),
name: Some("stable".into()),
..Cohort::default()
})
.extra_fields([("channel".to_string(), "stable".to_string())])
.build();
assert_eq!(app_set.get_apps(), vec![system_app, package_app, package2_app]);
}
#[fasync::run_singlethreaded(test)]
async fn test_get_eager_package_version_and_channel_fallback() {
let (proxy, mut stream) = create_proxy_and_stream::<CupMarker>().unwrap();
let stream_fut = async move {
match stream.next().await.unwrap() {
Ok(CupRequest::GetInfo { url, responder }) => {
assert_eq!(url.url, "fuchsia-pkg://example.com/package");
responder.send(&mut Ok(("abc".into(), "beta".into()))).unwrap();
}
request => panic!("Unexpected request: {:?}", request),
}
};
let stable_channel_config = ChannelConfig {
name: "stable".into(),
repo: "stable".into(),
appid: Some("1a2b3c4d".into()),
check_interval_secs: None,
};
let config = EagerPackageConfig {
url: UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.com/package").unwrap(),
flavor: Some("debug".into()),
channel_config: ChannelConfigs {
default_channel: Some("stable".into()),
known_channels: vec![stable_channel_config.clone()],
},
};
// unknown channel or invalid version fallback to default
let ((version, channel_config), ()) = future::join(
ClientConfiguration::get_eager_package_version_and_channel(&config, &Some(proxy)),
stream_fut,
)
.await;
assert_eq!(channel_config.unwrap(), stable_channel_config);
assert_eq!(version, MINIMUM_VALID_VERSION.into());
// GetInfoError fallback to default
let (proxy, mut stream) = create_proxy_and_stream::<CupMarker>().unwrap();
let stream_fut = async move {
match stream.next().await.unwrap() {
Ok(CupRequest::GetInfo { url, responder }) => {
assert_eq!(url.url, "fuchsia-pkg://example.com/package");
responder.send(&mut Err(GetInfoError::NotAvailable)).unwrap();
}
request => panic!("Unexpected request: {:?}", request),
}
};
let ((version, channel_config), ()) = future::join(
ClientConfiguration::get_eager_package_version_and_channel(&config, &Some(proxy)),
stream_fut,
)
.await;
assert_eq!(channel_config.unwrap(), stable_channel_config);
assert_eq!(version, MINIMUM_VALID_VERSION.into());
// no proxy fallback to default
let (version, channel_config) =
ClientConfiguration::get_eager_package_version_and_channel(&config, &None).await;
assert_eq!(channel_config.unwrap(), stable_channel_config);
assert_eq!(version, MINIMUM_VALID_VERSION.into());
}
}