use anyhow::{Context, Result, anyhow};
use nostr_sdk::prelude::*;
use rstest::*;
use test_harness::{Harness, PublishPatchSeriesOpts, PublishRepoOpts};
const ORIGINAL_TITLE: &str = "original proposal";
const ORIGINAL_DESCRIPTION: &str = "the proposal being revised";
const REVISION_TITLE: &str = "revised proposal";
const REVISION_DESCRIPTION: &str = "the revision";
struct Snapshot {
revision_cover_letter: Event,
revision_patch: Event,
original_cover_letter_id: EventId,
}
#[fixture]
async fn snapshot() -> Snapshot {
capture_snapshot()
.await
.expect("send_patch_revision fixture: capture_snapshot failed")
}
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 (_publisher, published) = harness.publish_repo(PublishRepoOpts::default()).await?;
let original = harness
.publish_patch_series(
&published,
PublishPatchSeriesOpts {
branch: Some("feature-1".into()),
commits: vec![
("a3.md".into(), "some content\n".into()),
("a4.md".into(), "some content\n".into()),
],
cover_letter: Some((ORIGINAL_TITLE.into(), ORIGINAL_DESCRIPTION.into())),
..Default::default()
},
)
.await?;
let original_cover_letter = original
.cover_letter_event
.clone()
.context("original proposal did not produce a cover-letter event")?;
let revision = harness
.publish_patch_series(
&published,
PublishPatchSeriesOpts {
branch: Some("feature-2".into()),
commits: vec![
("b3.md".into(), "some content\n".into()),
("b4.md".into(), "some content\n".into()),
],
cover_letter: Some((REVISION_TITLE.into(), REVISION_DESCRIPTION.into())),
in_reply_to: vec![original_cover_letter.id.to_hex()],
},
)
.await?;
let revision_cover_letter = revision
.cover_letter_event
.clone()
.context("revision did not produce a cover-letter event")?;
let first_commit_oid = revision
.commits
.first()
.cloned()
.context("revision returned no commits")?;
let revision_patch = find_patch_for_commit(&revision.patch_events, &first_commit_oid)?;
Ok(Snapshot {
revision_cover_letter,
revision_patch,
original_cover_letter_id: original_cover_letter.id,
})
}
fn find_patch_for_commit(patches: &[Event], oid: &str) -> Result<Event> {
patches
.iter()
.find(|e| tag_value(e, "commit").as_deref() == Some(oid))
.cloned()
.ok_or_else(|| anyhow!("no patch event found with commit tag {oid}"))
}
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()
.filter_map(|t| {
let s = t.as_slice();
if s.first().map(String::as_str) == Some(key) {
s.get(1).cloned()
} else {
None
}
})
.collect()
}
#[derive(Debug, Clone, Copy)]
enum Case {
TTagRoot,
TTagRevisionRoot,
ETagReplyToOriginal,
PatchETagRootIsRevisionCoverLetter,
}
#[rstest]
#[case::t_tag_root(Case::TTagRoot)]
#[case::t_tag_revision_root(Case::TTagRevisionRoot)]
#[case::e_tag_reply_to_original(Case::ETagReplyToOriginal)]
#[case::patch_e_tag_root_is_revision_cover_letter(Case::PatchETagRootIsRevisionCoverLetter)]
#[tokio::test]
async fn revision_tags(#[future] snapshot: Snapshot, #[case] case: Case) -> Result<()> {
let s = snapshot.await;
match case {
Case::TTagRoot => {
assert!(
tag_values(&s.revision_cover_letter, "t")
.iter()
.any(|v| v == "root"),
"revision cover letter should carry `t root`; tags were {:?}",
s.revision_cover_letter.tags,
);
}
Case::TTagRevisionRoot => {
let t_values = tag_values(&s.revision_cover_letter, "t");
assert!(
t_values
.iter()
.any(|v| v == "revision-root" || v == "root-revision"),
"revision cover letter should carry `t revision-root` (or the alias \
`root-revision`); t tags were {t_values:?}",
);
}
Case::ETagReplyToOriginal => {
let reply_e = s
.revision_cover_letter
.tags
.iter()
.find(|t| {
let v = t.as_slice();
v.first().map(String::as_str) == Some("e")
&& v.len() == 4
&& v.get(3).map(String::as_str) == Some("reply")
})
.ok_or_else(|| {
anyhow!(
"revision cover letter missing reply-marker `e` tag (tags: {:?})",
s.revision_cover_letter.tags
)
})?;
assert_eq!(
reply_e.as_slice().get(1).map(String::as_str),
Some(s.original_cover_letter_id.to_hex().as_str()),
"revision cover letter's `e <reply>` should point at the original proposal's root",
);
}
Case::PatchETagRootIsRevisionCoverLetter => {
let root_e = s
.revision_patch
.tags
.iter()
.find(|t| {
let v = t.as_slice();
v.first().map(String::as_str) == Some("e")
&& v.len() == 4
&& v.get(3).map(String::as_str) == Some("root")
})
.ok_or_else(|| {
anyhow!(
"revision per-commit patch missing root-marker `e` tag (tags: {:?})",
s.revision_patch.tags
)
})?;
assert_eq!(
root_e.as_slice().get(1).map(String::as_str),
Some(s.revision_cover_letter.id.to_hex().as_str()),
"revision per-commit patch's `e <root>` should point at the revision's own \
cover letter (not the original proposal's)",
);
}
}
Ok(())
}