Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I082de335ed2b2bf53d687d81db0901ef6a6a6964
230 lines
5.8 KiB
Rust
230 lines
5.8 KiB
Rust
//! Common test utilities for integration tests.
|
|
|
|
#![expect(clippy::expect_used)]
|
|
|
|
use std::{collections::HashMap, path::Path};
|
|
|
|
use tempfile::{NamedTempFile, TempDir};
|
|
use wiremock::{
|
|
Mock,
|
|
MockServer,
|
|
ResponseTemplate,
|
|
matchers::{header, method, path_regex},
|
|
};
|
|
|
|
/// Configuration for a mock repository.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MockRepo {
|
|
pub owner: String,
|
|
pub name: String,
|
|
pub archive_bytes: Vec<u8>,
|
|
}
|
|
|
|
impl MockRepo {
|
|
pub fn new(owner: &str, name: &str, archive_bytes: Vec<u8>) -> Self {
|
|
Self {
|
|
owner: owner.to_string(),
|
|
name: name.to_string(),
|
|
archive_bytes,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets up a mock Forgejo server with the given repositories.
|
|
pub async fn setup_mock_server(repos: Vec<MockRepo>) -> (MockServer, String) {
|
|
let server = MockServer::start().await;
|
|
|
|
// Group repos by organization
|
|
let mut org_repos: HashMap<String, Vec<MockRepo>> = HashMap::new();
|
|
for repo in repos {
|
|
org_repos.entry(repo.owner.clone()).or_default().push(repo);
|
|
}
|
|
|
|
// Set up organization repo listing endpoints
|
|
for (org, repos) in org_repos {
|
|
let repos_clone = repos.clone();
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path_regex(format!(r"/orgs/{}/repos", regex::escape(&org))))
|
|
.and(header("authorization", "token test-token"))
|
|
.and(header("accept", "application/json"))
|
|
.respond_with(move |_req: &wiremock::Request| {
|
|
let api_repos: Vec<serde_json::Value> = repos_clone
|
|
.iter()
|
|
.map(|r| {
|
|
serde_json::json!({
|
|
"name": r.name,
|
|
"default_branch": "main",
|
|
"owner": {"login": r.owner}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
ResponseTemplate::new(200).set_body_json(api_repos)
|
|
})
|
|
.mount(&server)
|
|
.await;
|
|
|
|
// Set up archive download endpoints for each repo
|
|
for repo in repos {
|
|
let archive_bytes = repo.archive_bytes.clone();
|
|
Mock::given(method("GET"))
|
|
.and(path_regex(format!(
|
|
r"/repos/{}/{}/archive/.*",
|
|
regex::escape(&repo.owner),
|
|
regex::escape(&repo.name)
|
|
)))
|
|
.and(header("authorization", "token test-token"))
|
|
.respond_with(move |_req: &wiremock::Request| {
|
|
ResponseTemplate::new(200)
|
|
.set_body_bytes(archive_bytes.clone())
|
|
.insert_header("content-type", "application/gzip")
|
|
})
|
|
.mount(&server)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
let url = server.uri();
|
|
(server, url)
|
|
}
|
|
|
|
/// Test context holding all temporary resources.
|
|
pub struct TestContext {
|
|
temp_dir: TempDir,
|
|
sink_dir: TempDir,
|
|
db_file: NamedTempFile,
|
|
config_file: NamedTempFile,
|
|
mock_url: String,
|
|
}
|
|
|
|
impl TestContext {
|
|
/// Creates a new test context with the given mock server URL.
|
|
pub fn new(mock_url: String) -> Self {
|
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let sink_dir = TempDir::new().expect("Failed to create sink dir");
|
|
let db_file = NamedTempFile::new().expect("Failed to create temp db file");
|
|
let config_file =
|
|
NamedTempFile::new().expect("Failed to create temp config file");
|
|
|
|
Self {
|
|
temp_dir,
|
|
sink_dir,
|
|
db_file,
|
|
config_file,
|
|
mock_url,
|
|
}
|
|
}
|
|
|
|
/// Writes a TOML configuration file for the test.
|
|
pub fn write_config(
|
|
&self,
|
|
orgs: &[String],
|
|
excludes: &[String],
|
|
concurrency: usize,
|
|
max_retries: u32,
|
|
backoff_ms: u64,
|
|
) {
|
|
let orgs_list = orgs
|
|
.iter()
|
|
.map(|o| format!("\"{o}\""))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let excludes_str = excludes
|
|
.iter()
|
|
.map(|e| format!("\"{e}\""))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
|
|
let config = format!(
|
|
r#"[service]
|
|
name = "test"
|
|
temp_dir = "{}"
|
|
state_db_path = "{}"
|
|
concurrency_limit = {concurrency}
|
|
|
|
[service.retry]
|
|
max_retries = {max_retries}
|
|
initial_backoff_ms = {backoff_ms}
|
|
|
|
[[source]]
|
|
type = "forgejo"
|
|
id = "test-source"
|
|
api_url = "{}"
|
|
token = "test-token"
|
|
|
|
[source.scope]
|
|
organizations = [{orgs_list}]
|
|
exclude_repos = [{excludes_str}]
|
|
|
|
[[sink]]
|
|
type = "filesystem"
|
|
id = "test-sink"
|
|
path = "{}"
|
|
verify_on_write = true
|
|
"#,
|
|
self.temp_dir.path().display(),
|
|
self.db_file.path().display(),
|
|
self.mock_url,
|
|
self.sink_dir.path().display()
|
|
);
|
|
|
|
std::fs::write(self.config_file.path(), config)
|
|
.expect("Failed to write config file");
|
|
}
|
|
|
|
/// Returns the path to the configuration file.
|
|
pub fn config_path(&self) -> &Path {
|
|
self.config_file.path()
|
|
}
|
|
|
|
/// Returns the path to the database file.
|
|
pub fn db_path(&self) -> &Path {
|
|
self.db_file.path()
|
|
}
|
|
}
|
|
|
|
/// Creates a minimal valid tar.gz archive containing a single file.
|
|
pub fn create_test_archive(seed: &str) -> Vec<u8> {
|
|
use std::io::Write;
|
|
|
|
let mut tar_builder = tar::Builder::new(Vec::new());
|
|
|
|
let content =
|
|
format!("Test content for seed: {seed}\nThis is deterministic.");
|
|
let mut header = tar::Header::new_gnu();
|
|
header
|
|
.set_path(format!("{seed}/test.txt"))
|
|
.expect("Failed to set tar path");
|
|
header.set_size(content.len() as u64);
|
|
header.set_mode(0o644);
|
|
header.set_cksum();
|
|
|
|
tar_builder
|
|
.append(&header, content.as_bytes())
|
|
.expect("Failed to append to tar");
|
|
|
|
let tar_data = tar_builder
|
|
.into_inner()
|
|
.expect("Failed to finish tar archive");
|
|
|
|
let mut encoder =
|
|
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
|
encoder
|
|
.write_all(&tar_data)
|
|
.expect("Failed to compress tar");
|
|
encoder.finish().expect("Failed to finish gzip")
|
|
}
|
|
|
|
/// Computes the blake3 hash of the given data.
|
|
#[allow(dead_code)]
|
|
pub fn compute_hash(data: &[u8]) -> String {
|
|
blake3::hash(data).to_hex().to_string()
|
|
}
|
|
|
|
/// Loads the configuration from the test context.
|
|
pub fn load_config(
|
|
ctx: &TestContext,
|
|
) -> anyhow::Result<konservejo::config::Config> {
|
|
konservejo::config::Config::from_file(ctx.config_path())
|
|
}
|