tests: initial integration test setup
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I082de335ed2b2bf53d687d81db0901ef6a6a6964
This commit is contained in:
parent
89d40109e5
commit
133d392df7
7 changed files with 721 additions and 5 deletions
230
tests/common/mod.rs
Normal file
230
tests/common/mod.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
//! 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())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue