use std::sync::Arc;
use anyhow::{Context, Result, bail};
use nostr_sdk::prelude::*;
use rstest::*;
use test_harness::Harness;
use tokio::sync::OnceCell;
#[tokio::test]
async fn bare_no_flags_errors_no_announcement() -> Result<()> {
let harness = Harness::builder(
env!("CARGO_BIN_EXE_ngit"),
env!("CARGO_BIN_EXE_git-remote-nostr"),
)
.with_relay("default")
.build()
.await?;
let (repo, _state) = harness.arrange_init_state_b_coordinate_only().await?;
let out = repo
.ngit(["init"])
.output()
.await
.context("failed to spawn ngit init")?;
assert!(
!out.status.success(),
"expected `ngit init` to fail in State B; exited successfully\n\
stdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains("no announcement found for coordinate"),
"expected coordinate-only error, got: {combined}",
);
Ok(())
}
#[tokio::test]
async fn defaults_still_errors_no_announcement() -> Result<()> {
let harness = Harness::builder(
env!("CARGO_BIN_EXE_ngit"),
env!("CARGO_BIN_EXE_git-remote-nostr"),
)
.with_relay("default")
.build()
.await?;
let (repo, _state) = harness.arrange_init_state_b_coordinate_only().await?;
let out = repo
.ngit(["init", "--defaults"])
.output()
.await
.context("failed to spawn ngit init --defaults")?;
assert!(
!out.status.success(),
"expected `ngit init --defaults` to fail in State B; exited successfully",
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.contains("no announcement found for coordinate"),
"expected coordinate-only error even with --defaults, got: {combined}",
);
Ok(())
}
struct Snapshot {
announcement: Event,
coordinate_identifier: String,
grasp_http_url: String,
}
static SNAPSHOT: OnceCell<Arc<Snapshot>> = OnceCell::const_new();
#[fixture]
async fn snapshot() -> Arc<Snapshot> {
SNAPSHOT
.get_or_init(|| async {
Arc::new(
capture_snapshot()
.await
.expect("init_state_coordinate_only fixture: capture_snapshot failed"),
)
})
.await
.clone()
}
async fn capture_snapshot() -> Result<Snapshot> {
let harness = Harness::builder(
env!("CARGO_BIN_EXE_ngit"),
env!("CARGO_BIN_EXE_git-remote-nostr"),
)
.with_relay("default")
.with_grasp_server("repo")
.build()
.await?;
let (repo, state) = harness.arrange_init_state_b_coordinate_only().await?;
let grasp = harness.grasp("repo");
let grasp_http_url = grasp.url().to_string();
let init_out = repo
.ngit(["init", "--force", "--grasp-server", &grasp_http_url])
.output()
.await
.context("failed to spawn ngit init --force --grasp-server")?;
if !init_out.status.success() {
bail!(
"ngit init exited non-zero ({:?})\nstdout: {}\nstderr: {}",
init_out.status,
String::from_utf8_lossy(&init_out.stdout),
String::from_utf8_lossy(&init_out.stderr),
);
}
let announcements = harness
.relay("default")
.events(
Filter::new()
.author(state.keys.public_key())
.kind(Kind::GitRepoAnnouncement),
)
.await?;
let announcement = announcements
.into_iter()
.find(|e| tag_value(e, "d").as_deref() == Some(state.coordinate_identifier.as_str()))
.with_context(|| {
format!(
"no kind-30617 with `d` = {:?} on the default relay after \
`ngit init --force`",
state.coordinate_identifier,
)
})?;
Ok(Snapshot {
announcement,
coordinate_identifier: state.coordinate_identifier,
grasp_http_url,
})
}
#[rstest]
#[tokio::test]
async fn identifier_from_coordinate(#[future] snapshot: Arc<Snapshot>) -> Result<()> {
let s = snapshot.await;
assert_eq!(
tag_value(&s.announcement, "d").as_deref(),
Some(s.coordinate_identifier.as_str()),
);
Ok(())
}
#[rstest]
#[tokio::test]
async fn name_defaults_to_identifier(#[future] snapshot: Arc<Snapshot>) -> Result<()> {
let s = snapshot.await;
assert_eq!(
tag_value(&s.announcement, "name").as_deref(),
Some(s.coordinate_identifier.as_str()),
);
Ok(())
}
#[rstest]
#[tokio::test]
async fn clone_url_from_grasp_server(#[future] snapshot: Arc<Snapshot>) -> Result<()> {
let s = snapshot.await;
let clone_urls = tag_values(&s.announcement, "clone");
let prefix = format!("{}/", s.grasp_http_url);
assert!(
clone_urls.iter().any(|u| u.starts_with(&prefix)),
"expected at least one clone url starting with {prefix}; got {clone_urls:?}",
);
Ok(())
}
fn tag_value(event: &Event, key: &str) -> Option<String> {
event.tags.iter().find_map(|t| {
let s = t.as_slice();
if s.first().map(String::as_str) == Some(key) {
s.get(1).cloned()
} else {
None
}
})
}
fn tag_values(event: &Event, key: &str) -> Vec<String> {
event
.tags
.iter()
.find(|t| t.as_slice().first().map(String::as_str) == Some(key))
.map(|t| t.as_slice()[1..].to_vec())
.unwrap_or_default()
}