fix: parsing maintainers from announcement event

This commit is contained in:
DanConwayDev
2025-11-26 09:56:44 +00:00
parent 30411a938d
commit a6edb42dfc
6 changed files with 1395 additions and 38 deletions
+75 -3
View File
@@ -129,6 +129,7 @@ pub struct AuditEventBuilder {
content: String,
tags: Vec<Tag>,
config: AuditConfig,
custom_timestamp: Option<Timestamp>,
}
impl AuditEventBuilder {
@@ -139,6 +140,7 @@ impl AuditEventBuilder {
content: content.into(),
tags: Vec::new(),
config,
custom_timestamp: None,
}
}
@@ -154,14 +156,49 @@ impl AuditEventBuilder {
self
}
/// Set a custom timestamp for the event
///
/// By default, events use the current time. Use this method to create
/// events with a specific timestamp, which is useful for testing
/// timestamp-based prioritization logic.
///
/// # Example
///
/// ```rust
/// use nostr_sdk::prelude::*;
/// use grasp_audit::{AuditConfig, AuditEventBuilder};
///
/// let config = AuditConfig::ci();
/// let keys = Keys::generate();
///
/// // Create an event with a past timestamp
/// let past_event = AuditEventBuilder::new(Kind::TextNote, "test", config)
/// .custom_time(Timestamp::from(1700000000))
/// .build(&keys)
/// .unwrap();
///
/// assert_eq!(past_event.created_at, Timestamp::from(1700000000));
/// ```
pub fn custom_time(mut self, timestamp: Timestamp) -> Self {
self.custom_timestamp = Some(timestamp);
self
}
/// Build the event with audit tags
pub fn build(self, keys: &Keys) -> anyhow::Result<Event> {
let mut all_tags = self.tags;
all_tags.extend(self.config.audit_tags());
let event = EventBuilder::new(self.kind, self.content)
.tags(all_tags)
.sign_with_keys(keys)?;
let builder = EventBuilder::new(self.kind, self.content).tags(all_tags);
// Apply custom timestamp if set
let builder = if let Some(timestamp) = self.custom_timestamp {
builder.custom_created_at(timestamp)
} else {
builder
};
let event = builder.sign_with_keys(keys)?;
Ok(event)
}
@@ -243,4 +280,39 @@ mod tests {
// Verify event is valid
assert!(event.verify().is_ok());
}
#[test]
fn test_custom_timestamp_applied() {
let config = AuditConfig::ci();
let keys = Keys::generate();
let custom_ts = Timestamp::from(1700000000);
// Build event with custom timestamp
let event = AuditEventBuilder::new(Kind::TextNote, "test with custom time", config.clone())
.custom_time(custom_ts)
.build(&keys)
.unwrap();
// Verify the custom timestamp was applied
assert_eq!(event.created_at, custom_ts);
// Verify event is still valid
assert!(event.verify().is_ok());
}
#[test]
fn test_default_timestamp_uses_current_time() {
let config = AuditConfig::ci();
let keys = Keys::generate();
let before = Timestamp::now();
let event = AuditEventBuilder::new(Kind::TextNote, "test default time", config.clone())
.build(&keys)
.unwrap();
let after = Timestamp::now();
// Event timestamp should be between before and after (inclusive)
assert!(event.created_at.as_u64() >= before.as_u64());
assert!(event.created_at.as_u64() <= after.as_u64());
}
}
+125
View File
@@ -286,6 +286,80 @@ impl AuditClient {
Ok(event)
}
/// Create a NIP-34 repository announcement event with maintainers
///
/// This helper creates a properly formatted NIP-34 announcement that will be
/// accepted by GRASP relays (which require events to list the relay in clone/relays tags).
/// This variant also includes a maintainers tag for push authorization testing.
///
/// # Arguments
/// * `test_name` - Name of the test (used to create unique repo identifier)
/// * `maintainer_pubkeys` - Hex pubkeys of maintainers who can push to the repository
///
/// # Returns
/// A built and signed Event ready to be sent to the relay
pub async fn create_repo_announcement_with_maintainers(
&self,
test_name: &str,
maintainer_pubkeys: &[String],
) -> Result<Event> {
// Get relay URL from client
let relay_url = self
.client
.relays()
.await
.keys()
.next()
.ok_or_else(|| anyhow!("No relay connected"))?
.to_string();
// Convert WebSocket URL to HTTP URL for clone tag
let http_url = relay_url
.replace("ws://", "http://")
.replace("wss://", "https://");
// Create unique repository identifier using UUID for consistency
let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]);
// Get npub for clone URL
let npub = self
.public_key()
.to_bech32()
.map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?;
// Build kind 30617 repository announcement with maintainers tag
let event = self
.event_builder(
Kind::GitRepoAnnouncement,
format!("Test repository for {}", test_name),
)
.tag(Tag::identifier(&repo_id))
.tag(Tag::custom(
TagKind::custom("name"),
vec![format!("{} Test Repository", test_name)],
))
.tag(Tag::custom(
TagKind::custom("description"),
vec![format!("Repository for {} testing", test_name)],
))
.tag(Tag::custom(
TagKind::custom("clone"),
vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
))
.tag(Tag::custom(
TagKind::custom("relays"),
vec![relay_url.clone()],
))
.tag(Tag::custom(
TagKind::custom("maintainers"),
maintainer_pubkeys.to_vec(),
))
.build(self.keys())
.map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?;
Ok(event)
}
/// Create an issue (kind 1621) that references a repository
///
/// # Arguments
@@ -456,4 +530,55 @@ mod tests {
"Missing custom tag value"
);
}
#[tokio::test]
async fn test_create_repo_announcement_with_maintainers() {
let config = AuditConfig::ci();
let client = AuditClient::new_test(config);
// Create test maintainer pubkeys (hex format)
let maintainer_pubkeys = vec![
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".to_string(),
"b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(),
];
// Note: We can't test create_repo_announcement_with_maintainers directly in unit tests
// because it requires a connected relay. Instead, we test the underlying event building
// with maintainers tag to verify the tag format is correct.
// Build an event with maintainers tag directly to test the tag format
let event = client
.event_builder(
Kind::GitRepoAnnouncement,
"Test repository",
)
.tag(Tag::identifier("test-repo"))
.tag(Tag::custom(
TagKind::custom("maintainers"),
maintainer_pubkeys.clone(),
))
.build(client.keys())
.unwrap();
// Verify the maintainers tag is present and correctly formatted
let maintainers_tag = event
.tags
.iter()
.find(|t| t.kind() == TagKind::custom("maintainers"));
assert!(
maintainers_tag.is_some(),
"Missing 'maintainers' tag in event"
);
// Verify the tag contains the maintainer pubkeys
let tag = maintainers_tag.unwrap();
let tag_vec: Vec<String> = tag.clone().to_vec();
// First element is "maintainers", rest are the pubkeys
assert_eq!(tag_vec[0], "maintainers");
assert_eq!(tag_vec.len(), 3, "Expected 3 elements: tag name + 2 pubkeys");
assert_eq!(tag_vec[1], maintainer_pubkeys[0]);
assert_eq!(tag_vec[2], maintainer_pubkeys[1]);
}
}
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -444,11 +444,16 @@ mod tests {
) -> Event {
let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])];
// Add maintainers as p tags
for maintainer_keys in maintainers {
// Add maintainers as a single "maintainers" tag per NIP-34
// Format: ["maintainers", "<pubkey1-hex>", "<pubkey2-hex>", ...]
if !maintainers.is_empty() {
let maintainer_pubkeys: Vec<String> = maintainers
.iter()
.map(|k| k.public_key().to_hex())
.collect();
tags.push(Tag::custom(
TagKind::p(),
vec![maintainer_keys.public_key().to_hex()],
TagKind::Custom("maintainers".into()),
maintainer_pubkeys,
));
}
+17 -7
View File
@@ -105,14 +105,19 @@ impl RepositoryAnnouncement {
})
.collect();
// Extract maintainers (other-user tags)
// Extract maintainers from "maintainers" tag per NIP-34
// Format: ["maintainers", "<pubkey1-hex>", "<pubkey2-hex>", ...]
let maintainers = event
.tags
.iter()
.filter(|t| t.kind() == TagKind::p())
.filter_map(|t| t.content())
.map(|s| s.to_string())
.collect();
.find(|tag| tag.as_slice().first().map(|s| s.as_str()) == Some("maintainers"))
.map(|tag| {
tag.as_slice()[1..] // Skip the "maintainers" tag name
.iter()
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default();
Ok(RepositoryAnnouncement {
event,
@@ -546,8 +551,12 @@ mod tests {
),
];
// Add maintainer
tags.push(Tag::public_key(maintainer_keys.public_key()));
// Add maintainer using NIP-34 "maintainers" tag format
// Format: ["maintainers", "<pubkey1-hex>", "<pubkey2-hex>", ...]
tags.push(Tag::custom(
nostr_sdk::TagKind::Custom("maintainers".into()),
vec![maintainer_keys.public_key().to_hex()],
));
let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT), "Test repository")
.tags(tags)
@@ -556,6 +565,7 @@ mod tests {
let announcement = RepositoryAnnouncement::from_event(event).unwrap();
assert_eq!(announcement.maintainers.len(), 1);
assert_eq!(announcement.maintainers[0], maintainer_keys.public_key().to_hex());
}
#[test]
-1
View File
@@ -65,6 +65,5 @@ macro_rules! isolated_push_test {
isolated_push_test!(test_push_authorized_by_owner_state);
isolated_push_test!(test_push_rejected_without_state_event);
isolated_push_test!(test_push_rejected_wrong_commit);
isolated_push_test!(test_recursive_maintainer_authorization);
isolated_push_test!(test_latest_state_event_used);
isolated_push_test!(test_non_maintainer_state_rejected);