use anyhow::anyhow;
use mockall::predicate;
use std::collections::BTreeMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use mithril_cardano_node_internal_database::entities::AncillaryFilesManifest;
use mithril_common::{
StdResult,
crypto_helper::ManifestSigner,
entities::{CompressionAlgorithm, FileUri},
};
use super::{DownloadEvent, FileDownloaderUri, MockFileDownloader};
type MockFileDownloaderBuilderReturningFunc = Box<
dyn FnMut(
&FileDownloaderUri,
u64,
&Path,
Option<CompressionAlgorithm>,
DownloadEvent,
) -> StdResult<()>
+ Send
+ 'static,
>;
pub struct MockFileDownloaderBuilder {
mock_file_downloader: Option<MockFileDownloader>,
times: usize,
param_file_downloader_uri: Option<FileDownloaderUri>,
param_target_dir: Option<PathBuf>,
param_compression_algorithm: Option<Option<CompressionAlgorithm>>,
returning_func: Option<MockFileDownloaderBuilderReturningFunc>,
}
#[derive(bon::Builder)]
pub struct FakeAncillaryFileBuilder {
files_in_manifest_to_create: Vec<String>,
files_not_in_manifest_to_create: Option<Vec<String>>,
sign_manifest: Option<ManifestSigner>,
}
impl Default for MockFileDownloaderBuilder {
fn default() -> Self {
Self {
mock_file_downloader: None,
times: 1,
param_file_downloader_uri: None,
param_target_dir: None,
param_compression_algorithm: Some(Some(CompressionAlgorithm::default())),
returning_func: None,
}
}
}
impl MockFileDownloaderBuilder {
pub fn from_mock(mock: MockFileDownloader) -> Self {
Self {
mock_file_downloader: Some(mock),
..Self::default()
}
}
pub fn with_success(self) -> Self {
self.with_returning(Box::new(|_, _, _, _, _| Ok(())))
}
pub fn with_failure(self) -> Self {
self.with_returning(Box::new(|_, _, _, _, _| {
Err(anyhow!("Download unpack failed"))
}))
}
pub fn with_success_and_create_fake_ancillary_files(
self,
fake_ancillary_file_builder: FakeAncillaryFileBuilder,
) -> Self {
self.with_returning(Box::new(move |_, _, target_dir: &Path, _, _| {
for filename in [
fake_ancillary_file_builder.files_in_manifest_to_create.as_slice(),
fake_ancillary_file_builder
.files_not_in_manifest_to_create
.as_ref()
.unwrap_or(&vec![])
.as_slice(),
]
.concat()
{
File::create(target_dir.join(filename)).unwrap();
}
let empty_file_sha256 = hex::encode(Sha256::digest(""));
let mut ancillary_manifest =
AncillaryFilesManifest::new_without_signature(BTreeMap::from_iter(
fake_ancillary_file_builder
.files_in_manifest_to_create
.iter()
.map(|filename| (PathBuf::from(filename), empty_file_sha256.clone())),
));
if let Some(manifest_signer) = &fake_ancillary_file_builder.sign_manifest {
ancillary_manifest
.set_signature(manifest_signer.sign(&ancillary_manifest.compute_hash()));
}
let ancillary_file =
File::create(target_dir.join(AncillaryFilesManifest::ANCILLARY_MANIFEST_FILE_NAME))
.unwrap();
serde_json::to_writer(ancillary_file, &ancillary_manifest).unwrap();
Ok(())
}))
}
pub fn with_times(self, times: usize) -> Self {
let mut self_mut = self;
self_mut.times = times;
self_mut
}
pub fn with_file_uri<T: AsRef<str>>(self, file_uri: T) -> Self {
let mut self_mut = self;
self_mut.param_file_downloader_uri = Some(FileDownloaderUri::FileUri(FileUri(
file_uri.as_ref().to_string(),
)));
self_mut
}
pub fn with_target_dir(self, target_dir: PathBuf) -> Self {
let mut self_mut = self;
self_mut.param_target_dir = Some(target_dir);
self_mut
}
pub fn with_compression(self, compression: Option<CompressionAlgorithm>) -> Self {
let mut self_mut = self;
self_mut.param_compression_algorithm = Some(compression);
self_mut
}
pub fn with_returning(self, returning_func: MockFileDownloaderBuilderReturningFunc) -> Self {
let mut self_mut = self;
self_mut.returning_func = Some(returning_func);
self_mut
}
pub fn build(self) -> MockFileDownloader {
let predicate_file_downloader_uri = predicate::function(move |u| {
self.param_file_downloader_uri
.as_ref()
.map(|x| x == u)
.unwrap_or(true)
});
let predicate_target_dir = predicate::function(move |u| {
self.param_target_dir.as_ref().map(|x| x == u).unwrap_or(true)
});
let predicate_compression_algorithm = predicate::function(move |u| {
self.param_compression_algorithm
.as_ref()
.map(|x| x == u)
.unwrap_or(true)
});
let predicate_download_event_type = predicate::always();
let mut mock_file_downloader = self.mock_file_downloader.unwrap_or_default();
mock_file_downloader
.expect_download_unpack()
.with(
predicate_file_downloader_uri,
predicate::always(),
predicate_target_dir,
predicate_compression_algorithm,
predicate_download_event_type,
)
.times(self.times)
.returning(self.returning_func.unwrap());
mock_file_downloader
}
pub fn next_call(self) -> Self {
let mock_file_downloader = self.build();
Self::from_mock(mock_file_downloader)
}
}
#[cfg(test)]
mod tests {
use mithril_common::{assert_dir_eq, temp_dir_create};
use crate::file_downloader::FileDownloader;
use super::*;
#[tokio::test]
async fn can_be_parametrized_to_create_fake_ancillary_files_with_signed_manifest() {
let target_dir = temp_dir_create!();
let ancillary_signer = ManifestSigner::create_deterministic_signer();
let verifier = ancillary_signer.verification_key();
let fake_ancillary_file_builder = FakeAncillaryFileBuilder::builder()
.files_in_manifest_to_create(vec!["file1.txt".to_string(), "file2.txt".to_string()])
.files_not_in_manifest_to_create(vec!["not_in_manifest.txt".to_string()])
.sign_manifest(ancillary_signer)
.build();
let mock_file_downloader = MockFileDownloaderBuilder::default()
.with_file_uri("http://ancillary.tar.gz")
.with_success_and_create_fake_ancillary_files(fake_ancillary_file_builder)
.build();
mock_file_downloader
.download_unpack(
&FileDownloaderUri::FileUri(FileUri("http://ancillary.tar.gz".to_string())),
0,
&target_dir,
Some(CompressionAlgorithm::default()),
DownloadEvent::Ancillary {
download_id: "whatever".to_string(),
},
)
.await
.unwrap();
let ancillary_manifest: AncillaryFilesManifest = serde_json::from_reader(
File::open(target_dir.join(AncillaryFilesManifest::ANCILLARY_MANIFEST_FILE_NAME))
.unwrap(),
)
.unwrap();
const EMPTY_SHA256_HASH: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
assert_eq!(
ancillary_manifest.signable_manifest.data,
BTreeMap::from([
(PathBuf::from("file1.txt"), EMPTY_SHA256_HASH.to_string()),
(PathBuf::from("file2.txt"), EMPTY_SHA256_HASH.to_string()),
])
);
verifier
.verify(
&ancillary_manifest.compute_hash(),
&ancillary_manifest.signature().expect("Manifest should be signed"),
)
.expect("Signed manifest should have a valid signature");
assert_dir_eq!(
&target_dir,
format!(
"* {}
* file1.txt
* file2.txt
* not_in_manifest.txt",
AncillaryFilesManifest::ANCILLARY_MANIFEST_FILE_NAME
)
);
}
}