use self::support::*;
use anyhow::{Context, Result};
use rand_core::OsRng;
use reqwest::StatusCode;
use std::{
borrow::Cow,
fs,
time::{Duration, SystemTime},
};
use url::Url;
use warg_api::v1::{
content::{ContentSource, ContentSourcesResponse},
fetch::{FetchPackageNamesRequest, FetchPackageNamesResponse},
ledger::{LedgerSource, LedgerSourceContentType, LedgerSourcesResponse},
package::PublishRecordRequest,
paths,
};
use warg_client::{
api,
storage::{PublishEntry, PublishInfo},
ClientError, Config,
};
use warg_crypto::{
hash::{HashAlgorithm, Sha256},
signing::PrivateKey,
Encode, Signable,
};
use warg_protocol::{
package::{PackageEntry, PackageRecord, PACKAGE_RECORD_VERSION},
registry::{LogId, PackageName},
ProtoEnvelope, ProtoEnvelopeBody, Version,
};
use wit_component::DecodedWasm;
mod support;
mod memory;
#[cfg(feature = "postgres")]
mod postgres;
async fn test_initial_checkpoint(config: &Config) -> Result<()> {
let client = api::Client::new(config.home_url.as_ref().unwrap(), None)?;
let ts_checkpoint = client.latest_checkpoint(None).await?;
let checkpoint = &ts_checkpoint.as_ref().checkpoint;
assert_eq!(checkpoint.log_length, 1);
let operator_key = test_operator_key();
assert_eq!(
ts_checkpoint.key_id().to_string(),
operator_key.public_key().fingerprint().to_string()
);
warg_protocol::registry::TimestampedCheckpoint::verify(
&operator_key.public_key(),
&ts_checkpoint.as_ref().encode(),
ts_checkpoint.signature(),
)?;
Ok(())
}
async fn test_component_publishing(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:component";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
let digest = publish_component(
&client,
&name,
PACKAGE_VERSION,
"(component)",
true,
&signing_key,
)
.await?;
let download = client
.download(&name, &PACKAGE_VERSION.parse()?)
.await?
.context("failed to resolve package")?;
assert_eq!(download.digest, digest);
assert_eq!(download.version, PACKAGE_VERSION.parse()?);
assert_eq!(
download.path,
config
.content_dir
.as_ref()
.unwrap()
.join("sha256")
.join(download.digest.to_string().strip_prefix("sha256:").unwrap())
);
match wit_component::decode(&fs::read(download.path).context("failed to read component")?)? {
DecodedWasm::Component(..) => {}
_ => panic!("expected component"),
}
assert!(client.download(&name, &"0.2.0".parse()?).await?.is_none());
Ok(())
}
async fn test_package_yanking(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:yankee";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
publish(
&client,
&name,
PACKAGE_VERSION,
wat::parse_str("(component)")?,
true,
&signing_key,
)
.await?;
let record_id = client
.publish_with_info(
&signing_key,
PublishInfo {
name: name.clone(),
head: None,
entries: vec![PublishEntry::Yank {
version: PACKAGE_VERSION.parse()?,
}],
},
)
.await?;
client
.wait_for_publish(&name, &record_id, Duration::from_millis(100))
.await?;
let opt = client.download(&name, &PACKAGE_VERSION.parse()?).await?;
assert!(opt.is_none(), "expected no download, got {opt:?}");
Ok(())
}
async fn test_wit_publishing(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:wit-package";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
let digest = publish_wit(
&client,
&name,
PACKAGE_VERSION,
&format!("package {PACKAGE_NAME};\nworld foo {{}}"),
true,
&signing_key,
)
.await?;
let download = client
.download(&name, &PACKAGE_VERSION.parse()?)
.await?
.context("failed to resolve package")?;
assert_eq!(download.digest, digest);
assert_eq!(download.version, PACKAGE_VERSION.parse()?);
assert_eq!(
download.path,
config
.content_dir
.as_ref()
.unwrap()
.join("sha256")
.join(download.digest.to_string().strip_prefix("sha256:").unwrap())
);
match wit_component::decode(&fs::read(download.path).context("failed to read component")?)? {
DecodedWasm::WitPackage(..) => {}
_ => panic!("expected wit package"),
}
assert!(client.download(&name, &"0.2.0".parse()?).await?.is_none());
Ok(())
}
async fn test_wasm_content_policy(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:bad-content";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
match publish(
&client,
&name,
PACKAGE_VERSION,
Vec::new(),
true,
&signing_key,
)
.await
.expect_err("expected publish to fail")
.downcast::<ClientError>()
{
Ok(ClientError::PublishRejected {
name: rejected_name,
record_id,
reason,
}) => {
assert_eq!(name, rejected_name);
assert_eq!(
reason,
"content is not valid WebAssembly: unexpected end-of-file (at offset 0x0)"
);
match client
.wait_for_publish(&name, &record_id, Duration::from_millis(100))
.await
.expect_err("expected wait for publish to fail")
{
ClientError::PublishRejected {
name: rejected_name,
record_id: other,
reason,
} => {
assert_eq!(name, rejected_name);
assert_eq!(record_id, other);
assert_eq!(
reason,
"content with digest `sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` was rejected by policy: content is not valid WebAssembly: unexpected end-of-file (at offset 0x0)"
);
}
_ => panic!("expected a content policy rejection error"),
}
}
_ => panic!("expected a content policy rejection error"),
}
Ok(())
}
async fn test_unauthorized_signing_key(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:unauthorized-key";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
publish_component(
&client,
&name,
PACKAGE_VERSION,
"(component)",
true,
&signing_key,
)
.await?;
let signing_key = PrivateKey::from(p256::ecdsa::SigningKey::random(&mut OsRng));
let message = format!(
"{:#}",
publish_component(&client, &name, "0.2.0", "(component)", false, &signing_key,)
.await
.expect_err("expected publish to fail")
);
assert!(
message.contains("not authorized to publish to package `test:unauthorized-key`"),
"unexpected error message: {message}"
);
Ok(())
}
async fn test_unknown_signing_key(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:unknown-key";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
publish_component(
&client,
&name,
PACKAGE_VERSION,
"(component)",
true,
&signing_key,
)
.await?;
let signing_key = PrivateKey::from(p256::ecdsa::SigningKey::random(&mut OsRng));
let message = format!(
"{:#}",
publish_component(&client, &name, "0.2.0", "(component)", false, &signing_key,)
.await
.expect_err("expected publish to fail")
);
assert!(
message.contains("unknown key id"),
"unexpected error message: {message}"
);
Ok(())
}
async fn test_invalid_signature(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:invalid-signature";
let name = PackageName::new(PACKAGE_NAME)?;
let log_id = LogId::package_log::<Sha256>(&name);
let url = Url::parse(config.home_url.as_ref().unwrap())?
.join(&paths::publish_package_record(&log_id))
.unwrap();
let signing_key = test_signing_key();
let record = ProtoEnvelope::signed_contents(
&signing_key,
PackageRecord {
prev: None,
version: PACKAGE_RECORD_VERSION,
timestamp: SystemTime::now(),
entries: vec![PackageEntry::Init {
hash_algorithm: warg_crypto::hash::HashAlgorithm::Sha256,
key: signing_key.public_key(),
}],
},
)?;
let body = PublishRecordRequest {
package_name: Cow::Borrowed(&name),
record: Cow::Owned(ProtoEnvelopeBody::from(record)),
content_sources: Default::default(),
};
let mut body = serde_json::to_value(&body).unwrap();
body["record"]["signature"] = serde_json::Value::String("ecdsa-p256:MEUCIQCzWZBW6ux9LecP66Y+hjmLZTP/hZVz7puzlPTXcRT2wwIgQZO7nxP0nugtw18MwHZ26ROFWcJmgCtKOguK031Y1D0=".to_string());
let client = reqwest::Client::new();
let response = client
.post(url)
.json(&serde_json::to_value(&body).unwrap())
.send()
.await?;
let status = response.status();
let body = response.text().await?;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"unexpected response from server: {status}\n{body}",
);
assert!(
body.contains("verification failed"),
"unexpected response body: {body}"
);
Ok(())
}
async fn test_custom_content_url(config: &Config) -> Result<()> {
const PACKAGE_NAME: &str = "test:custom-content-url";
const PACKAGE_VERSION: &str = "0.1.0";
let name = PackageName::new(PACKAGE_NAME)?;
let client = create_client(config).await?;
let signing_key = test_signing_key();
let digest = publish_component(
&client,
&name,
PACKAGE_VERSION,
"(component)",
true,
&signing_key,
)
.await?;
let package = client.package(&name).await?;
package
.state
.release(&Version::parse(PACKAGE_VERSION)?)
.expect("expected the package version to exist");
let client = api::Client::new(config.home_url.as_ref().unwrap(), None)?;
let ContentSourcesResponse { content_sources } = client.content_sources(None, &digest).await?;
assert_eq!(content_sources.len(), 1);
let sources = content_sources
.get(&digest)
.expect("expected content source to be provided for the requested digest");
assert_eq!(sources.len(), 1);
let expected_url = format!(
"https://example.com/content/{digest}",
digest = digest.to_string().replace(':', "-")
);
match &sources[0] {
ContentSource::HttpGet { url, .. } => {
assert_eq!(url, &expected_url);
}
}
Ok(())
}
async fn test_fetch_package_names(config: &Config) -> Result<()> {
let name_1 = PackageName::new("test:component")?;
let log_id_1 = LogId::package_log::<Sha256>(&name_1);
let url = Url::parse(config.home_url.as_ref().unwrap())?
.join(paths::fetch_package_names())
.unwrap();
let body = FetchPackageNamesRequest {
packages: Cow::Owned(vec![log_id_1.clone()]),
};
let client = reqwest::Client::new();
let response = client
.post(url)
.json(&serde_json::to_value(&body).unwrap())
.send()
.await?;
let status = response.status();
let names_resp = response.json::<FetchPackageNamesResponse>().await?;
assert_eq!(
status,
StatusCode::OK,
"unexpected response from server: {status}",
);
let lookup_name_1 = names_resp.packages.get(&log_id_1);
assert_eq!(
lookup_name_1,
Some(&Some(name_1.clone())),
"fetch of package name {name_1} mismatched to {lookup_name_1:?}"
);
Ok(())
}
async fn test_get_ledger(config: &Config) -> Result<()> {
let client = api::Client::new(config.home_url.as_ref().unwrap(), None)?;
let ts_checkpoint = client.latest_checkpoint(None).await?;
let checkpoint = &ts_checkpoint.as_ref().checkpoint;
let url = Url::parse(config.home_url.as_ref().unwrap())?
.join(paths::ledger_sources())
.unwrap();
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let status = response.status();
let ledger_sources = response.json::<LedgerSourcesResponse>().await?;
assert_eq!(
status,
StatusCode::OK,
"unexpected response from server: {status}",
);
let hash_algorithm = ledger_sources.hash_algorithm;
assert_eq!(
hash_algorithm,
HashAlgorithm::Sha256,
"unexpected hash_algorithm: {hash_algorithm}",
);
let sources_len = ledger_sources.sources.len();
assert_eq!(sources_len, 1, "unexpected sources length: {sources_len}",);
let LedgerSource {
first_registry_index,
last_registry_index,
url,
content_type,
..
} = ledger_sources.sources.first().unwrap();
assert_eq!(
content_type,
&LedgerSourceContentType::Packed,
"unexpected ledger source content type",
);
assert_eq!(
*first_registry_index, 0,
"unexpected ledger source first registry index: {first_registry_index}",
);
assert_eq!(
*last_registry_index,
checkpoint.log_length - 1,
"unexpected ledger source last registry index: {last_registry_index}",
);
let url = Url::parse(config.home_url.as_ref().unwrap())?
.join(url)
.unwrap();
let response = client.get(url).send().await?;
let status = response.status();
assert_eq!(
status,
StatusCode::OK,
"unexpected response from server: {status}",
);
let bytes = response.bytes().await?;
let bytes_len = bytes.len();
assert_eq!(
bytes_len,
checkpoint.log_length * 64,
"unexpected response body length for ledger source from server: {bytes_len}",
);
Ok(())
}