mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-06 07:28:23 +00:00
test(private-repos): exercise the HTTP authentication boundary
The private-service implementation spans command-line configuration, HTTP routing, canonical repository identity, and credential validation. Unit tests of those pieces cannot prove that the production subprocess composes them into a fail-closed endpoint. Extend TestRelay with an explicit private-service constructor and exercise the real process over HTTP. The scenario verifies that missing and non-member credentials receive the same empty Nostr 401 challenge while a configured member crosses the authentication boundary. A nonexistent repository is intentional: authentication is the behavior under test, and successful authorization must remain distinguishable only by proceeding to ordinary Git routing. NIP-42 protocol framing remains covered by its focused state-machine tests; duplicating that protocol through an SDK client is excluded from this process-level HTTP regression. Validation: cargo fmt --check passed; cargo test --locked --test private_mode passed (45 tests including the new subprocess scenario).
This commit is contained in:
@@ -87,6 +87,7 @@ struct RelayOptions {
|
||||
rejected_hot_cache_duration_secs: Option<u64>,
|
||||
relay_max_subscriptions: Option<usize>,
|
||||
sync_recursive_descendant_limit: Option<usize>,
|
||||
private_members: Option<String>,
|
||||
/// Run with the production outbound target policy (reject non-global
|
||||
/// event-directed sync targets). The fixture default is permissive
|
||||
/// because the entire test infrastructure lives on loopback.
|
||||
@@ -128,6 +129,23 @@ impl TestRelay {
|
||||
Self::start_internal(port::reserve_port(), RelayOptions::default()).await
|
||||
}
|
||||
|
||||
/// Start one GRASP-08 private service whose static membership contains
|
||||
/// `member`.
|
||||
pub async fn start_private(member: &nostr_sdk::prelude::PublicKey) -> Self {
|
||||
Self::start_internal(
|
||||
port::reserve_port(),
|
||||
RelayOptions {
|
||||
private_members: Some(
|
||||
member
|
||||
.to_bech32()
|
||||
.expect("Failed to encode private test member"),
|
||||
),
|
||||
..RelayOptions::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start relay with sync from another relay (bootstrap relay)
|
||||
///
|
||||
/// # Example
|
||||
@@ -638,6 +656,14 @@ impl TestRelay {
|
||||
if let Some(ref fallback_relays) = options.sync_plus_fallback_relays {
|
||||
cmd.env("NGIT_SYNC_PLUS_FALLBACK_RELAYS", fallback_relays);
|
||||
}
|
||||
if let Some(ref private_members) = options.private_members {
|
||||
cmd.env("NGIT_PRIVATE_MODE", "true")
|
||||
.env("NGIT_PRIVATE_MEMBERS", private_members)
|
||||
.env(
|
||||
"NGIT_PRIVATE_PUBLIC_ORIGIN",
|
||||
format!("http://{bind_address}"),
|
||||
);
|
||||
}
|
||||
|
||||
// The test infrastructure runs entirely on loopback, which the
|
||||
// production outbound target policy rejects for event-directed sync.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
mod common;
|
||||
|
||||
use base64::Engine;
|
||||
use common::TestRelay;
|
||||
use nostr_sdk::prelude::{EventBuilder, FinalizeEvent, Keys, Kind, Tag, ToBech32};
|
||||
use reqwest::header::{AUTHORIZATION, WWW_AUTHENTICATE};
|
||||
|
||||
fn credential(keys: &Keys, repository_url: &str) -> String {
|
||||
let event = EventBuilder::new(Kind::HttpAuth, "")
|
||||
.tags(vec![
|
||||
Tag::parse(["u", repository_url]).expect("URL tag"),
|
||||
Tag::parse(["method", "GET"]).expect("method tag"),
|
||||
])
|
||||
.finalize(keys)
|
||||
.expect("signed NIP-98 credential");
|
||||
format!(
|
||||
"Nostr {}",
|
||||
base64::engine::general_purpose::STANDARD.encode(event.as_json())
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_git_endpoint_requires_a_service_member() {
|
||||
let member = Keys::generate();
|
||||
let outsider = Keys::generate();
|
||||
let repository_owner = Keys::generate()
|
||||
.public_key()
|
||||
.to_bech32()
|
||||
.expect("repository owner npub");
|
||||
let relay = TestRelay::start_private(&member.public_key()).await;
|
||||
let repository_url = format!(
|
||||
"http://{}/{repository_owner}/private-repository.git",
|
||||
relay.domain()
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
for authorization in [None, Some(credential(&outsider, &repository_url))] {
|
||||
let mut request = client.get(&repository_url);
|
||||
if let Some(authorization) = authorization {
|
||||
request = request.header(AUTHORIZATION, authorization);
|
||||
}
|
||||
let response = request.send().await.expect("private Git response");
|
||||
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(WWW_AUTHENTICATE)
|
||||
.expect("Nostr challenge")
|
||||
.to_str()
|
||||
.expect("ASCII challenge"),
|
||||
format!("Nostr realm=\"{}\", method=\"GET\"", relay.domain())
|
||||
);
|
||||
assert!(response.bytes().await.expect("response body").is_empty());
|
||||
}
|
||||
|
||||
let response = client
|
||||
.get(&repository_url)
|
||||
.header(AUTHORIZATION, credential(&member, &repository_url))
|
||||
.send()
|
||||
.await
|
||||
.expect("authenticated Git response");
|
||||
assert_ne!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
|
||||
relay.stop().await;
|
||||
}
|
||||
Reference in New Issue
Block a user