mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-06 07:28:23 +00:00
fix: parsing maintainers from announcement event
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user