Files
ngit-grasp/tests/audit_probe_state.rs
T
DanConwayDev 9fa726bbe1 fix(audit): scope state to the selected repository's maintainers
Fetching kind 30618 by identifier alone mixed independently owned repositories. A newer unrelated state could supply the wrong expected refs even though both repositories were valid on the relay.

Fetch announcements alongside state, select each author's NIP-01-preferred announcement, and resolve current publishers from the selected coordinate. Support reciprocal legacy/indexed roles, transitive confirmation, departures and explicit lead forwarding; reject unresolved lead paths. Verify and scope events before selecting the newest authorized state. Keep the audit crate independent of ngit-grasp.

Authority uses the probed relay's visible announcements. This does not change server authorization, add cross-relay discovery or provide an atomic Git/Nostr snapshot.

Validation: 95 grasp-audit tests pass (5 ignored), including membership and ordering regressions. The local-relay integration target passes all 55 tests, with an end-to-end same-identifier collision and injected stale refs. Workspace all-target Clippy and formatting pass.

Assisted-by: GPT-6
2026-09-17 16:32:16 +00:00

116 lines
4.0 KiB
Rust

mod common;
use common::{publish_served_audit_repo_with_state, wait_for_event_served, TestRelay};
use grasp_audit::{probe::run_probe, AuditClient, AuditConfig, DETERMINISTIC_COMMIT_HASH};
use nostr_sdk::prelude::*;
use std::time::Duration;
#[tokio::test]
async fn probe_scopes_same_identifier_states_and_reports_extra_refs() {
let relay = TestRelay::start().await;
let client = AuditClient::new(relay.url(), AuditConfig::shared())
.await
.unwrap();
let (announcement, identifier, initial_state) =
publish_served_audit_repo_with_state(&client, "probe-scope").await;
let owner_path = relay.git_data_path().join(
ngit_grasp::nostr::events::RepositoryAnnouncement::from_event(announcement.clone())
.unwrap()
.repo_path(),
);
let unrelated = Keys::generate();
let foreign_announcement =
common::create_repo_announcement(&unrelated, &[&relay.domain()], &identifier);
// Ensure the probe selects the owner's announcement, while the unrelated
// repository supplies the newest state for this identifier.
let foreign_announcement = EventBuilder::new(foreign_announcement.kind, "")
.tags(foreign_announcement.tags.iter().cloned())
.custom_created_at(Timestamp::from_secs(announcement.created_at.as_secs() - 1))
.finalize(&unrelated)
.unwrap();
let foreign_path = relay.git_data_path().join(
ngit_grasp::nostr::events::RepositoryAnnouncement::from_event(foreign_announcement.clone())
.unwrap()
.repo_path(),
);
std::fs::create_dir_all(foreign_path.parent().unwrap()).unwrap();
let cloned = grasp_audit::git_command()
.args(["clone", "--bare"])
.arg(&owner_path)
.arg(&foreign_path)
.output()
.unwrap();
assert!(
cloned.status.success(),
"{}",
String::from_utf8_lossy(&cloned.stderr)
);
client
.send_event(foreign_announcement.clone())
.await
.unwrap();
let foreign_state = EventBuilder::new(Kind::RepoState, "")
.tags([
Tag::identifier(&identifier),
Tag::custom("refs/heads/foreign", [DETERMINISTIC_COMMIT_HASH]),
Tag::custom("HEAD", ["ref: refs/heads/foreign"]),
])
.custom_created_at(Timestamp::from_secs(initial_state.created_at.as_secs() + 1))
.finalize(&unrelated)
.unwrap();
client.send_event(foreign_state.clone()).await.unwrap();
for event in [&foreign_announcement, &foreign_state, &initial_state] {
wait_for_event_served(relay.url(), &event.id, Duration::from_secs(10))
.await
.unwrap();
}
let report = run_probe(relay.url(), None, true, 10, 30).await;
let selection = report
.checks
.iter()
.find(|check| check.name == "serves_latest_announcement")
.unwrap();
assert_eq!(
selection.detail.as_deref(),
Some(
format!(
"{}/{}",
client.public_key().to_bech32().unwrap(),
identifier
)
.as_str()
)
);
let check = report
.checks
.iter()
.find(|check| check.name == "git_refs_match_state")
.unwrap();
assert!(check.passed, "{check:?}");
for name in ["refs/heads/stale", "refs/tags/stale"] {
let result = grasp_audit::git_command()
.arg("--git-dir")
.arg(&owner_path)
.args(["update-ref", name, DETERMINISTIC_COMMIT_HASH])
.output()
.unwrap();
assert!(result.status.success());
}
let report = run_probe(relay.url(), None, true, 10, 30).await;
let check = report
.checks
.iter()
.find(|check| check.name == "git_refs_match_state")
.unwrap();
assert!(!check.passed);
let error = check.error.as_deref().unwrap();
assert!(
error.contains("refs/heads/stale: unexpected ref"),
"{error}"
);
assert!(error.contains("refs/tags/stale: unexpected ref"), "{error}");
relay.stop().await;
}