mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
add repository creation
This commit is contained in:
Generated
+2
@@ -684,6 +684,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
@@ -1217,6 +1218,7 @@ dependencies = [
|
||||
"nostr-sdk 0.44.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
|
||||
@@ -49,6 +49,7 @@ thiserror = "1.0"
|
||||
tokio-test = "0.4"
|
||||
grasp-audit = { path = "grasp-audit" }
|
||||
url = "2.5"
|
||||
tempfile = "3"
|
||||
|
||||
[lib]
|
||||
name = "ngit_grasp"
|
||||
|
||||
@@ -43,3 +43,4 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
tempfile = "3"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
pub mod event_acceptance_policy;
|
||||
pub mod nip01_smoke;
|
||||
pub mod nip11_document;
|
||||
pub mod repository_creation;
|
||||
|
||||
pub use event_acceptance_policy::EventAcceptancePolicyTests;
|
||||
pub use nip01_smoke::Nip01SmokeTests;
|
||||
pub use nip11_document::Nip11DocumentTests;
|
||||
pub use repository_creation::RepositoryCreationTests;
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
//! GRASP-01 Repository Creation Tests
|
||||
//!
|
||||
//! Tests that verify bare Git repositories are created when repository announcements
|
||||
//! are accepted by the relay.
|
||||
//!
|
||||
//! ## Test Coverage
|
||||
//!
|
||||
//! - Repository creation on valid announcement
|
||||
//! - Idempotent creation (no error if repo already exists)
|
||||
//! - Proper directory structure (<npub>/<identifier>.git)
|
||||
//! - Bare repository validation (has HEAD, config, objects, refs)
|
||||
//!
|
||||
//! ## Running Tests
|
||||
//!
|
||||
//! ```bash
|
||||
//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
|
||||
//! ```
|
||||
|
||||
use crate::{AuditClient, TestContext, FixtureKind, TestResult};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::path::Path;
|
||||
|
||||
/// Test suite for repository creation
|
||||
pub struct RepositoryCreationTests;
|
||||
|
||||
impl RepositoryCreationTests {
|
||||
/// Test that a bare repository is created when a valid announcement is accepted
|
||||
///
|
||||
/// This test:
|
||||
/// 1. Sends a valid repository announcement via TestContext
|
||||
/// 2. Verifies the announcement was accepted
|
||||
/// 3. Checks that a bare git repository was created at the expected path
|
||||
pub async fn test_bare_repo_created_on_announcement(
|
||||
client: &AuditClient,
|
||||
git_data_dir: &Path,
|
||||
) -> TestResult {
|
||||
let test_name = "test_bare_repo_created_on_announcement";
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Use TestContext to create and send repository announcement
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must be created when announcement is accepted",
|
||||
).fail(&format!("Failed to create repo fixture: {}", e)),
|
||||
};
|
||||
|
||||
// Wait a bit for repository creation
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
// Extract repo identifier and npub from announcement
|
||||
let repo_id = match repo.tags.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
{
|
||||
Some(id) => id.to_string(),
|
||||
None => return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must be created when announcement is accepted",
|
||||
).fail("Repository announcement missing d tag"),
|
||||
};
|
||||
|
||||
let npub = match repo.pubkey.to_bech32() {
|
||||
Ok(n) => n,
|
||||
Err(e) => return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must be created when announcement is accepted",
|
||||
).fail(&format!("Failed to convert pubkey to npub: {}", e)),
|
||||
};
|
||||
|
||||
// Check if repository was created
|
||||
let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
|
||||
|
||||
if !is_bare_repository(&repo_path) {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must be created when announcement is accepted",
|
||||
).fail(&format!("Bare repository not found at: {}", repo_path.display()));
|
||||
}
|
||||
|
||||
TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must be created when announcement is accepted",
|
||||
).pass()
|
||||
}
|
||||
|
||||
/// Test that repository creation is idempotent
|
||||
///
|
||||
/// This test:
|
||||
/// 1. Sends a repository announcement (creates repo) via TestContext
|
||||
/// 2. Sends the same announcement again
|
||||
/// 3. Verifies no error occurs and repo still exists
|
||||
pub async fn test_repo_creation_idempotent(
|
||||
client: &AuditClient,
|
||||
git_data_dir: &Path,
|
||||
) -> TestResult {
|
||||
let test_name = "test_repo_creation_idempotent";
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Create and send repository announcement first time via TestContext
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Repository creation must be idempotent",
|
||||
).fail(&format!("Failed to create repo fixture: {}", e)),
|
||||
};
|
||||
|
||||
// Wait for repository creation
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
// Send the same announcement again (should be idempotent)
|
||||
if let Err(e) = client.send_event(repo.clone()).await {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Repository creation must be idempotent",
|
||||
).fail(&format!("Second send failed (not idempotent): {}", e));
|
||||
}
|
||||
|
||||
// Wait again
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
// Verify repository still exists and is valid
|
||||
let repo_id = repo.tags.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.ok_or("Missing d tag")
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let npub = repo.pubkey.to_bech32().unwrap();
|
||||
let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
|
||||
|
||||
if !is_bare_repository(&repo_path) {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Repository creation must be idempotent",
|
||||
).fail("Repository not found after second send");
|
||||
}
|
||||
|
||||
TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Repository creation must be idempotent",
|
||||
).pass()
|
||||
}
|
||||
|
||||
/// Test that the repository has the correct structure
|
||||
///
|
||||
/// This test verifies:
|
||||
/// 1. Repository is at <git_data_path>/<npub>/<identifier>.git
|
||||
/// 2. Repository is bare (no working directory)
|
||||
/// 3. Repository has required git structure (HEAD, config, objects/, refs/)
|
||||
pub async fn test_bare_repo_structure(
|
||||
client: &AuditClient,
|
||||
git_data_dir: &Path,
|
||||
) -> TestResult {
|
||||
let test_name = "test_bare_repo_structure";
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Create and send repository announcement via TestContext
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail(&format!("Failed to create repo fixture: {}", e)),
|
||||
};
|
||||
|
||||
// Wait for repository creation
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
// Extract repo identifier and npub
|
||||
let repo_id = repo.tags.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.ok_or("Missing d tag")
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let npub = repo.pubkey.to_bech32().unwrap();
|
||||
|
||||
// Verify correct path structure: <git_data_path>/<npub>/<identifier>.git
|
||||
let expected_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
|
||||
|
||||
if !expected_path.exists() {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail(&format!("Repository not at expected path: {}", expected_path.display()));
|
||||
}
|
||||
|
||||
// Verify it's a bare repository with correct structure
|
||||
if !expected_path.join("HEAD").is_file() {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail("Missing HEAD file");
|
||||
}
|
||||
|
||||
if !expected_path.join("config").is_file() {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail("Missing config file");
|
||||
}
|
||||
|
||||
if !expected_path.join("objects").is_dir() {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail("Missing objects/ directory");
|
||||
}
|
||||
|
||||
if !expected_path.join("refs").is_dir() {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail("Missing refs/ directory");
|
||||
}
|
||||
|
||||
// Verify the helper function agrees
|
||||
if !is_bare_repository(&expected_path) {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).fail("Helper function does not recognize repository as bare");
|
||||
}
|
||||
|
||||
TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Bare repository must have correct structure",
|
||||
).pass()
|
||||
}
|
||||
|
||||
/// Test that repository creation cleanup works
|
||||
///
|
||||
/// This test:
|
||||
/// 1. Creates multiple repositories via TestContext
|
||||
/// 2. Verifies they exist
|
||||
/// 3. Ensures test cleanup removes them (via TempDir drop)
|
||||
pub async fn test_repo_cleanup(
|
||||
client: &AuditClient,
|
||||
git_data_dir: &Path,
|
||||
) -> TestResult {
|
||||
let test_name = "test_repo_cleanup";
|
||||
let ctx = TestContext::new(client);
|
||||
|
||||
// Create multiple repositories via TestContext
|
||||
let mut repo_paths = Vec::new();
|
||||
|
||||
for _i in 0..3 {
|
||||
let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Test cleanup must remove created repositories",
|
||||
).fail(&format!("Failed to create repo fixture: {}", e)),
|
||||
};
|
||||
|
||||
// Extract path
|
||||
let repo_id = repo.tags.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let npub = repo.pubkey.to_bech32().unwrap();
|
||||
let path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
|
||||
repo_paths.push(path);
|
||||
}
|
||||
|
||||
// Wait for all repositories to be created
|
||||
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||
|
||||
// Verify all repositories exist
|
||||
for path in &repo_paths {
|
||||
if !is_bare_repository(path) {
|
||||
return TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Test cleanup must remove created repositories",
|
||||
).fail(&format!("Repository not created at: {}", path.display()));
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Actual cleanup happens when the TestRelay's TempDir is dropped
|
||||
// This test just verifies that repositories were created successfully
|
||||
// The integration test framework will verify cleanup
|
||||
|
||||
TestResult::new(
|
||||
test_name,
|
||||
"GRASP-01",
|
||||
"Test cleanup must remove created repositories",
|
||||
).pass()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to check if a path is a valid bare git repository
|
||||
///
|
||||
/// A bare repository must have:
|
||||
/// - HEAD file
|
||||
/// - config file
|
||||
/// - objects/ directory
|
||||
/// - refs/ directory
|
||||
pub fn is_bare_repository(path: &Path) -> bool {
|
||||
if !path.exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for required bare repository components
|
||||
let has_head = path.join("HEAD").is_file();
|
||||
let has_config = path.join("config").is_file();
|
||||
let has_objects = path.join("objects").is_dir();
|
||||
let has_refs = path.join("refs").is_dir();
|
||||
|
||||
has_head && has_config && has_objects && has_refs
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn test_is_bare_repository_detects_valid_repo() {
|
||||
// Create a temporary bare repository for testing
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let repo_path = temp_dir.path().join("test.git");
|
||||
|
||||
// Initialize a bare repository
|
||||
Command::new("git")
|
||||
.args(&["init", "--bare", repo_path.to_str().unwrap()])
|
||||
.output()
|
||||
.expect("Failed to create test repository");
|
||||
|
||||
// Verify our helper function detects it
|
||||
assert!(
|
||||
is_bare_repository(&repo_path),
|
||||
"Should detect valid bare repository"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_bare_repository_rejects_non_repo() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
assert!(
|
||||
!is_bare_repository(temp_dir.path()),
|
||||
"Should reject non-repository directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_bare_repository_rejects_nonexistent() {
|
||||
let path = Path::new("/nonexistent/path/to/repo.git");
|
||||
assert!(
|
||||
!is_bare_repository(path),
|
||||
"Should reject nonexistent path"
|
||||
);
|
||||
}
|
||||
}
|
||||
+74
-9
@@ -3,7 +3,7 @@
|
||||
/// This module integrates nostr-relay-builder with NIP-34 validation logic
|
||||
/// preserved from the original implementation.
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use nostr::nips::nip19::ToBech32;
|
||||
@@ -13,7 +13,7 @@ use nostr_relay_builder::prelude::*;
|
||||
|
||||
use crate::config::{Config, DatabaseBackend};
|
||||
use crate::nostr::events::{
|
||||
validate_announcement, validate_state, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE,
|
||||
validate_announcement, validate_state, RepositoryAnnouncement, KIND_REPOSITORY_ANNOUNCEMENT, KIND_REPOSITORY_STATE,
|
||||
};
|
||||
|
||||
/// NIP-34 Write Policy with Full GRASP-01 Event Validation
|
||||
@@ -30,16 +30,53 @@ use crate::nostr::events::{
|
||||
pub struct Nip34WritePolicy {
|
||||
domain: String,
|
||||
database: Arc<MemoryDatabase>,
|
||||
git_data_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Nip34WritePolicy {
|
||||
pub fn new(domain: impl Into<String>, database: Arc<MemoryDatabase>) -> Self {
|
||||
pub fn new(domain: impl Into<String>, database: Arc<MemoryDatabase>, git_data_path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
domain: domain.into(),
|
||||
database,
|
||||
git_data_path: git_data_path.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a bare git repository if it doesn't exist
|
||||
/// Path format: <git_data_path>/<npub>/<identifier>.git
|
||||
fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> {
|
||||
let repo_path = self.git_data_path.join(&announcement.repo_path());
|
||||
|
||||
// Check if repository already exists
|
||||
if repo_path.exists() {
|
||||
tracing::debug!("Repository already exists at {}", repo_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create parent directory (npub directory)
|
||||
let parent = repo_path.parent().ok_or_else(|| {
|
||||
format!("Invalid repository path: {}", repo_path.display())
|
||||
})?;
|
||||
|
||||
std::fs::create_dir_all(parent).map_err(|e| {
|
||||
format!("Failed to create directory {}: {}", parent.display(), e)
|
||||
})?;
|
||||
|
||||
// Initialize bare repository using git command
|
||||
let output = std::process::Command::new("git")
|
||||
.args(&["init", "--bare", repo_path.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute git init: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("git init failed: {}", stderr));
|
||||
}
|
||||
|
||||
tracing::info!("Created bare repository at {}", repo_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract all reference tags from an event (a, A, q, e, E)
|
||||
/// Returns (addressable_refs, event_refs)
|
||||
fn extract_reference_tags(event: &Event) -> (Vec<String>, Vec<EventId>) {
|
||||
@@ -269,11 +306,35 @@ impl WritePolicy for Nip34WritePolicy {
|
||||
match event.kind.as_u16() {
|
||||
KIND_REPOSITORY_ANNOUNCEMENT => match validate_announcement(event, &domain) {
|
||||
Ok(_) => {
|
||||
tracing::debug!(
|
||||
"Accepted repository announcement: {}",
|
||||
event_id_str
|
||||
);
|
||||
PolicyResult::Accept
|
||||
// Parse announcement to get repository details
|
||||
match RepositoryAnnouncement::from_event(event.clone()) {
|
||||
Ok(announcement) => {
|
||||
// Try to create bare repository if it doesn't exist
|
||||
if let Err(e) = self.ensure_bare_repository(&announcement) {
|
||||
tracing::warn!(
|
||||
"Failed to create bare repository for {}: {}",
|
||||
event_id_str,
|
||||
e
|
||||
);
|
||||
// Note: We still accept the event even if repo creation fails
|
||||
// The git operation failure shouldn't prevent event acceptance
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"Accepted repository announcement: {}",
|
||||
event_id_str
|
||||
);
|
||||
PolicyResult::Accept
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to parse repository announcement {}: {}",
|
||||
event_id_str,
|
||||
e
|
||||
);
|
||||
PolicyResult::Reject(format!("Failed to parse announcement: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
@@ -432,7 +493,11 @@ pub fn create_relay(config: &Config) -> Result<LocalRelay> {
|
||||
// Clone Arc for the write policy so both relay and policy can access the database
|
||||
let builder = RelayBuilder::default()
|
||||
.database(database.clone())
|
||||
.write_policy(Nip34WritePolicy::new(&config.domain, database.clone()));
|
||||
.write_policy(Nip34WritePolicy::new(
|
||||
&config.domain,
|
||||
database.clone(),
|
||||
&config.git_data_path,
|
||||
));
|
||||
|
||||
tracing::info!(
|
||||
"Relay configured with GRASP-01 validation for domain: {}",
|
||||
|
||||
+35
-2
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! Provides automatic relay lifecycle management for integration tests.
|
||||
|
||||
use nostr_sdk::ToBech32;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
@@ -9,11 +11,12 @@ use tokio::time::sleep;
|
||||
/// Test relay fixture that manages relay lifecycle
|
||||
///
|
||||
/// Automatically starts and stops the ngit-grasp relay for testing.
|
||||
/// Uses a random port to avoid conflicts.
|
||||
/// Uses a random port to avoid conflicts and cleans up created repositories.
|
||||
pub struct TestRelay {
|
||||
process: Child,
|
||||
url: String,
|
||||
port: u16,
|
||||
git_data_dir: tempfile::TempDir,
|
||||
}
|
||||
|
||||
impl TestRelay {
|
||||
@@ -40,6 +43,10 @@ impl TestRelay {
|
||||
let bind_address = format!("127.0.0.1:{}", port);
|
||||
let url = format!("ws://127.0.0.1:{}", port);
|
||||
|
||||
// Create temporary directory for git repositories
|
||||
let git_data_dir = tempfile::tempdir()
|
||||
.expect("Failed to create temporary git data directory");
|
||||
|
||||
// Use the built binary directly (faster than cargo run)
|
||||
let binary_path = std::env::current_exe()
|
||||
.expect("Failed to get current exe")
|
||||
@@ -49,17 +56,29 @@ impl TestRelay {
|
||||
.expect("Failed to get grandparent dir")
|
||||
.join("ngit-grasp");
|
||||
|
||||
// Generate a test owner npub (using a random keypair)
|
||||
let test_keys = nostr_sdk::Keys::generate();
|
||||
let test_npub = test_keys.public_key().to_bech32()
|
||||
.expect("Failed to generate test npub");
|
||||
|
||||
// Start the relay process
|
||||
let process = Command::new(&binary_path)
|
||||
.env("NGIT_BIND_ADDRESS", &bind_address)
|
||||
.env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
|
||||
.env("NGIT_GIT_DATA_PATH", git_data_dir.path())
|
||||
.env("NGIT_OWNER_NPUB", &test_npub)
|
||||
.env("RUST_LOG", "warn") // Less logging during tests
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("Failed to start relay process");
|
||||
|
||||
let relay = Self { process, url, port };
|
||||
let relay = Self {
|
||||
process,
|
||||
url,
|
||||
port,
|
||||
git_data_dir,
|
||||
};
|
||||
|
||||
// Wait for relay to be ready
|
||||
relay.wait_for_ready().await;
|
||||
@@ -82,6 +101,20 @@ impl TestRelay {
|
||||
format!("127.0.0.1:{}", self.port)
|
||||
}
|
||||
|
||||
/// Get the git data directory path
|
||||
pub fn git_data_dir(&self) -> &std::path::Path {
|
||||
self.git_data_dir.path()
|
||||
}
|
||||
|
||||
/// Get the expected repository path for a given npub and repo identifier
|
||||
///
|
||||
/// Repositories are stored at: <git_data_dir>/<npub>/<identifier>.git
|
||||
pub fn repo_path(&self, npub: &str, identifier: &str) -> PathBuf {
|
||||
self.git_data_dir.path()
|
||||
.join(npub)
|
||||
.join(format!("{}.git", identifier))
|
||||
}
|
||||
|
||||
/// Wait for the relay to be ready to accept connections
|
||||
async fn wait_for_ready(&self) {
|
||||
let max_attempts = 50; // 5 seconds total
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//! Repository Creation Integration Tests
|
||||
//!
|
||||
//! Tests that verify bare Git repositories are created when repository announcements
|
||||
//! are accepted by ngit-grasp relay.
|
||||
//!
|
||||
//! # Test Strategy
|
||||
//!
|
||||
//! - Each test runs in complete isolation with its own fresh relay instance
|
||||
//! - Uses macro to eliminate boilerplate while maintaining test isolation
|
||||
//! - Calls individual test methods from grasp-audit for minimal duplication
|
||||
//! - Automatic cleanup via TestRelay fixture (removes container and temp dirs)
|
||||
//!
|
||||
//! # Running Tests
|
||||
//!
|
||||
//! ```bash
|
||||
//! # Run all repository creation tests
|
||||
//! cargo test --test repository_creation
|
||||
//!
|
||||
//! # Run specific test
|
||||
//! cargo test --test repository_creation test_bare_repo_created_on_announcement
|
||||
//!
|
||||
//! # With output
|
||||
//! cargo test --test repository_creation -- --nocapture
|
||||
//! ```
|
||||
|
||||
mod common;
|
||||
|
||||
use common::TestRelay;
|
||||
use grasp_audit::*;
|
||||
use grasp_audit::specs::grasp01::RepositoryCreationTests;
|
||||
|
||||
/// Macro to generate isolated integration tests
|
||||
///
|
||||
/// Each test runs with its own fresh relay instance to ensure complete isolation.
|
||||
/// This eliminates issues with leftover repositories and ensures clean state.
|
||||
macro_rules! isolated_test {
|
||||
($test_name:ident) => {
|
||||
#[tokio::test]
|
||||
async fn $test_name() {
|
||||
let relay = TestRelay::start().await;
|
||||
let config = AuditConfig::ci();
|
||||
let client = AuditClient::new(relay.url(), config)
|
||||
.await
|
||||
.expect("Failed to create audit client");
|
||||
|
||||
let result = RepositoryCreationTests::$test_name(&client, relay.git_data_dir()).await;
|
||||
|
||||
relay.stop().await;
|
||||
|
||||
assert!(
|
||||
result.passed,
|
||||
"{} failed: {}",
|
||||
stringify!($test_name),
|
||||
result.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Generate isolated tests for all repository creation tests
|
||||
isolated_test!(test_bare_repo_created_on_announcement);
|
||||
isolated_test!(test_repo_creation_idempotent);
|
||||
isolated_test!(test_bare_repo_structure);
|
||||
isolated_test!(test_repo_cleanup);
|
||||
Reference in New Issue
Block a user