mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
fix some clippy fmt warnings
This commit is contained in:
@@ -8,12 +8,12 @@ use grasp_audit::*;
|
||||
async fn main() -> Result<()> {
|
||||
// Create audit config for CI testing
|
||||
let config = AuditConfig::ci();
|
||||
|
||||
|
||||
println!("GRASP Audit Example");
|
||||
println!("==================");
|
||||
println!("Audit Run ID: {}", config.run_id);
|
||||
println!();
|
||||
|
||||
|
||||
// Connect to relay
|
||||
println!("Connecting to relay at ws://localhost:7000...");
|
||||
let client = match AuditClient::new("ws://localhost:7000", config).await {
|
||||
@@ -26,28 +26,28 @@ async fn main() -> Result<()> {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if !client.is_connected().await {
|
||||
eprintln!("Not connected to relay");
|
||||
return Err(anyhow!("Connection failed"));
|
||||
}
|
||||
|
||||
|
||||
println!("✓ Connected");
|
||||
println!();
|
||||
|
||||
|
||||
// Run NIP-01 smoke tests
|
||||
println!("Running NIP-01 smoke tests...");
|
||||
println!();
|
||||
|
||||
|
||||
let results = specs::Nip01SmokeTests::run_all(&client).await;
|
||||
|
||||
|
||||
// Print results
|
||||
results.print_report();
|
||||
|
||||
|
||||
// Exit with error if tests failed
|
||||
if !results.all_passed() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+44
-40
@@ -7,13 +7,13 @@ use nostr_sdk::prelude::*;
|
||||
pub struct AuditConfig {
|
||||
/// Unique ID for this audit run
|
||||
pub run_id: String,
|
||||
|
||||
|
||||
/// Mode: CI (isolated) or Production (live)
|
||||
pub mode: AuditMode,
|
||||
|
||||
|
||||
/// Cleanup timestamp (events can be cleaned after this)
|
||||
pub cleanup_after: Timestamp,
|
||||
|
||||
|
||||
/// Whether to actually create events or just query
|
||||
pub read_only: bool,
|
||||
}
|
||||
@@ -23,7 +23,7 @@ pub struct AuditConfig {
|
||||
pub enum AuditMode {
|
||||
/// Isolated CI/CD tests - only see own events
|
||||
CI,
|
||||
|
||||
|
||||
/// Production audit - see all events, minimal writes
|
||||
Production,
|
||||
}
|
||||
@@ -39,7 +39,7 @@ impl AuditConfig {
|
||||
read_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create config for production audit
|
||||
pub fn production() -> Self {
|
||||
let run_id = format!("prod-audit-{}", Timestamp::now().as_u64());
|
||||
@@ -47,10 +47,10 @@ impl AuditConfig {
|
||||
run_id,
|
||||
mode: AuditMode::Production,
|
||||
cleanup_after: Timestamp::now() + 300, // 5 minutes from now
|
||||
read_only: true, // Default to read-only for production
|
||||
read_only: true, // Default to read-only for production
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create config with custom run ID
|
||||
pub fn with_run_id(run_id: String, mode: AuditMode) -> Self {
|
||||
Self {
|
||||
@@ -60,7 +60,7 @@ impl AuditConfig {
|
||||
read_only: mode == AuditMode::Production,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get audit tags that are automatically added to all events
|
||||
///
|
||||
/// These tags are automatically added to all events created via [`AuditEventBuilder`].
|
||||
@@ -102,22 +102,22 @@ impl AuditConfig {
|
||||
/// ```
|
||||
pub fn audit_tags(&self) -> Vec<Tag> {
|
||||
use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
|
||||
|
||||
|
||||
// Use "t" tags for categorization (standard NIP-01 hashtag type)
|
||||
let t_tag = SingleLetterTag::lowercase(Alphabet::T);
|
||||
|
||||
|
||||
vec![
|
||||
Tag::custom(TagKind::SingleLetter(t_tag), vec!["grasp-audit-test-event"]),
|
||||
Tag::custom(
|
||||
TagKind::SingleLetter(t_tag),
|
||||
vec!["grasp-audit-test-event"]
|
||||
vec![format!("audit-{}", self.run_id)],
|
||||
),
|
||||
Tag::custom(
|
||||
TagKind::SingleLetter(t_tag),
|
||||
vec![format!("audit-{}", self.run_id)]
|
||||
),
|
||||
Tag::custom(
|
||||
TagKind::SingleLetter(t_tag),
|
||||
vec![format!("audit-cleanup-after-{}", self.cleanup_after.as_u64())]
|
||||
vec![format!(
|
||||
"audit-cleanup-after-{}",
|
||||
self.cleanup_after.as_u64()
|
||||
)],
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -141,28 +141,28 @@ impl AuditEventBuilder {
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add a tag
|
||||
pub fn tag(mut self, tag: Tag) -> Self {
|
||||
self.tags.push(tag);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Add multiple tags
|
||||
pub fn tags(mut self, tags: Vec<Tag>) -> Self {
|
||||
self.tags.extend(tags);
|
||||
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)?;
|
||||
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
@@ -170,7 +170,7 @@ impl AuditEventBuilder {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_ci_config() {
|
||||
let config = AuditConfig::ci();
|
||||
@@ -178,7 +178,7 @@ mod tests {
|
||||
assert!(!config.read_only);
|
||||
assert!(config.run_id.starts_with("ci-"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_production_config() {
|
||||
let config = AuditConfig::production();
|
||||
@@ -186,18 +186,18 @@ mod tests {
|
||||
assert!(config.read_only);
|
||||
assert!(config.run_id.starts_with("prod-audit-"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_audit_tags() {
|
||||
use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
|
||||
|
||||
|
||||
let config = AuditConfig::ci();
|
||||
let tags = config.audit_tags();
|
||||
|
||||
|
||||
assert_eq!(tags.len(), 3);
|
||||
|
||||
|
||||
let t_tag = SingleLetterTag::lowercase(Alphabet::T);
|
||||
|
||||
|
||||
// All tags should be "t" tags (hashtags)
|
||||
for tag in &tags {
|
||||
if let TagKind::SingleLetter(letter) = tag.kind() {
|
||||
@@ -206,36 +206,40 @@ mod tests {
|
||||
panic!("Expected SingleLetter tag");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for "t" tag with "grasp-audit-test-event"
|
||||
assert!(tags.iter().any(|t| {
|
||||
t.content() == Some("grasp-audit-test-event")
|
||||
}));
|
||||
|
||||
assert!(tags
|
||||
.iter()
|
||||
.any(|t| { t.content() == Some("grasp-audit-test-event") }));
|
||||
|
||||
// Check for "t" tag with "audit-{run_id}"
|
||||
assert!(tags.iter().any(|t| {
|
||||
t.content().map(|c| c.starts_with("audit-ci-")).unwrap_or(false)
|
||||
t.content()
|
||||
.map(|c| c.starts_with("audit-ci-"))
|
||||
.unwrap_or(false)
|
||||
}));
|
||||
|
||||
|
||||
// Check for "t" tag with "audit-cleanup-after-{timestamp}"
|
||||
assert!(tags.iter().any(|t| {
|
||||
t.content().map(|c| c.starts_with("audit-cleanup-after-")).unwrap_or(false)
|
||||
t.content()
|
||||
.map(|c| c.starts_with("audit-cleanup-after-"))
|
||||
.unwrap_or(false)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_audit_event_builder() {
|
||||
let config = AuditConfig::ci();
|
||||
let keys = Keys::generate();
|
||||
|
||||
|
||||
let event = AuditEventBuilder::new(Kind::TextNote, "test", config.clone())
|
||||
.tag(Tag::custom(TagKind::Custom("test".into()), vec!["value"]))
|
||||
.build(&keys)
|
||||
.unwrap();
|
||||
|
||||
|
||||
// Should have our custom tag + 3 audit tags
|
||||
assert!(event.tags.len() >= 4);
|
||||
|
||||
|
||||
// Verify event is valid
|
||||
assert!(event.verify().is_ok());
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ enum Commands {
|
||||
/// Relay URL (e.g., ws://localhost:7000)
|
||||
#[arg(short, long)]
|
||||
relay: String,
|
||||
|
||||
|
||||
/// Mode: ci or production
|
||||
#[arg(short, long, default_value = "ci")]
|
||||
mode: String,
|
||||
|
||||
|
||||
/// Spec to test (nip01-smoke, all)
|
||||
#[arg(short, long, default_value = "nip01-smoke")]
|
||||
spec: String,
|
||||
@@ -35,12 +35,12 @@ async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive(tracing::Level::INFO.into())
|
||||
.add_directive(tracing::Level::INFO.into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
|
||||
match cli.command {
|
||||
Commands::Audit { relay, mode, spec } => {
|
||||
let config = match mode.as_str() {
|
||||
@@ -48,7 +48,7 @@ async fn main() -> Result<()> {
|
||||
"production" => AuditConfig::production(),
|
||||
_ => return Err(anyhow!("Invalid mode: {}. Use 'ci' or 'production'", mode)),
|
||||
};
|
||||
|
||||
|
||||
println!("🔍 GRASP Audit Tool");
|
||||
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
println!("Relay: {}", relay);
|
||||
@@ -57,17 +57,18 @@ async fn main() -> Result<()> {
|
||||
println!("Run ID: {}", config.run_id);
|
||||
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
println!();
|
||||
|
||||
|
||||
println!("Connecting to relay...");
|
||||
let client = AuditClient::new(&relay, config).await
|
||||
let client = AuditClient::new(&relay, config)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to connect to relay: {}", e))?;
|
||||
|
||||
|
||||
if !client.is_connected().await {
|
||||
return Err(anyhow!("Could not establish connection to relay"));
|
||||
}
|
||||
|
||||
|
||||
println!("✓ Connected\n");
|
||||
|
||||
|
||||
let results = match spec.as_str() {
|
||||
"nip01-smoke" => {
|
||||
println!("Running NIP-01 smoke tests...\n");
|
||||
@@ -77,11 +78,16 @@ async fn main() -> Result<()> {
|
||||
println!("Running all tests...\n");
|
||||
specs::Nip01SmokeTests::run_all(&client).await
|
||||
}
|
||||
_ => return Err(anyhow!("Unknown spec: {}. Use 'nip01-smoke' or 'all'", spec)),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"Unknown spec: {}. Use 'nip01-smoke' or 'all'",
|
||||
spec
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
results.print_report();
|
||||
|
||||
|
||||
if !results.all_passed() {
|
||||
println!("❌ Some tests failed");
|
||||
std::process::exit(1);
|
||||
@@ -90,6 +96,6 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+104
-70
@@ -24,32 +24,32 @@ impl AuditClient {
|
||||
keys,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a new audit client
|
||||
pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> {
|
||||
let keys = Keys::generate();
|
||||
let client = Client::new(keys.clone());
|
||||
|
||||
|
||||
// Add relay and connect
|
||||
client.add_relay(relay_url).await?;
|
||||
client.connect().await;
|
||||
|
||||
|
||||
// Wait for connection to establish (with retries)
|
||||
let mut attempts = 0;
|
||||
let mut connected = false;
|
||||
while attempts < 20 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
|
||||
let relays = client.relays().await;
|
||||
connected = relays.values().any(|r| r.is_connected());
|
||||
|
||||
|
||||
if connected {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
attempts += 1;
|
||||
}
|
||||
|
||||
|
||||
// Verify we actually connected
|
||||
if !connected {
|
||||
return Err(anyhow!(
|
||||
@@ -68,22 +68,22 @@ impl AuditClient {
|
||||
relay_url
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Give it a bit more time to stabilize
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
config,
|
||||
keys,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Get the public key for this audit client
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.keys.public_key()
|
||||
}
|
||||
|
||||
|
||||
/// Check if connected to relay
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
// Check if we have any connected relays
|
||||
@@ -95,29 +95,29 @@ impl AuditClient {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
/// Send an event (with audit tags automatically added)
|
||||
pub async fn send_event(&self, event: Event) -> Result<EventId> {
|
||||
if self.config.read_only {
|
||||
return Err(anyhow!("Client is in read-only mode"));
|
||||
}
|
||||
|
||||
|
||||
let output = self.client.send_event(&event).await?;
|
||||
let event_id = *output.id();
|
||||
|
||||
|
||||
// Check if any relay rejected the event and return the error message
|
||||
if !output.failed.is_empty() {
|
||||
// Get the first failed relay error message
|
||||
let (relay_url, error) = output.failed.iter().next().unwrap();
|
||||
return Err(anyhow!("Relay {} rejected event: {}", relay_url, error));
|
||||
}
|
||||
|
||||
|
||||
// Wait a bit for event to propagate
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
|
||||
Ok(event_id)
|
||||
}
|
||||
|
||||
|
||||
/// Create an event builder that automatically includes audit tags
|
||||
///
|
||||
/// All events built through this method will automatically have audit tags appended
|
||||
@@ -153,11 +153,11 @@ impl AuditClient {
|
||||
pub fn event_builder(&self, kind: Kind, content: impl Into<String>) -> AuditEventBuilder {
|
||||
AuditEventBuilder::new(kind, content, self.config.clone())
|
||||
}
|
||||
|
||||
|
||||
/// Query events, optionally filtered to this audit run
|
||||
pub async fn query(&self, mut filter: Filter) -> Result<Vec<Event>> {
|
||||
use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
|
||||
|
||||
|
||||
if self.config.mode == AuditMode::CI {
|
||||
// In CI mode, only see our own audit events
|
||||
// Filter by "t" tags (hashtags)
|
||||
@@ -167,14 +167,15 @@ impl AuditClient {
|
||||
.custom_tag(t_tag, format!("audit-{}", self.config.run_id));
|
||||
}
|
||||
// In Production mode, see all events (no filter modification)
|
||||
|
||||
let events = self.client
|
||||
|
||||
let events = self
|
||||
.client
|
||||
.fetch_events(filter, Duration::from_secs(5))
|
||||
.await?;
|
||||
|
||||
|
||||
Ok(events.into_iter().collect())
|
||||
}
|
||||
|
||||
|
||||
/// Subscribe to events with a callback
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
@@ -183,27 +184,25 @@ impl AuditClient {
|
||||
) -> Result<Vec<Event>> {
|
||||
let timeout = timeout.unwrap_or(Duration::from_secs(5));
|
||||
let mut all_events = Vec::new();
|
||||
|
||||
|
||||
for filter in filters {
|
||||
let events = self.client
|
||||
.fetch_events(filter, timeout)
|
||||
.await?;
|
||||
let events = self.client.fetch_events(filter, timeout).await?;
|
||||
all_events.extend(events.into_iter());
|
||||
}
|
||||
|
||||
|
||||
Ok(all_events)
|
||||
}
|
||||
|
||||
|
||||
/// Get the underlying nostr client (for advanced usage)
|
||||
pub fn client(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
|
||||
/// Get the keys (for signing custom events)
|
||||
pub fn keys(&self) -> &Keys {
|
||||
&self.keys
|
||||
}
|
||||
|
||||
|
||||
/// Create a NIP-34 repository announcement event
|
||||
///
|
||||
/// This helper creates a properly formatted NIP-34 announcement that will be
|
||||
@@ -216,37 +215,58 @@ impl AuditClient {
|
||||
/// A built and signed Event ready to be sent to the relay
|
||||
pub async fn create_repo_announcement(&self, test_name: &str) -> Result<Event> {
|
||||
// Get relay URL from client
|
||||
let relay_url = self.client.relays().await
|
||||
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()
|
||||
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
|
||||
let event = self.event_builder(Kind::GitRepoAnnouncement, format!("Test repository for {}", test_name))
|
||||
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("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()],
|
||||
))
|
||||
.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
|
||||
@@ -265,29 +285,31 @@ impl AuditClient {
|
||||
additional_tags: Vec<Tag>,
|
||||
) -> Result<Event> {
|
||||
// Extract repo_id from the d tag
|
||||
let repo_id = repo_event.tags.iter()
|
||||
let repo_id = repo_event
|
||||
.tags
|
||||
.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.ok_or_else(|| anyhow!("Repository event must have a 'd' tag"))?
|
||||
.to_string();
|
||||
|
||||
|
||||
let repo_pubkey = repo_event.pubkey;
|
||||
let a_tag_value = format!("30617:{}:{}", repo_pubkey, repo_id);
|
||||
|
||||
|
||||
let mut tags = vec![
|
||||
Tag::custom(TagKind::custom("a"), vec![a_tag_value]),
|
||||
Tag::custom(TagKind::custom("subject"), vec![issue_title]),
|
||||
];
|
||||
|
||||
|
||||
// Add any additional tags
|
||||
tags.extend(additional_tags);
|
||||
|
||||
|
||||
self.event_builder(Kind::Custom(1621), content)
|
||||
.tags(tags)
|
||||
.build(self.keys())
|
||||
.map_err(|e| anyhow!("Failed to build issue event: {}", e))
|
||||
}
|
||||
|
||||
|
||||
/// Create a NIP-22 comment (kind 1111) for an event
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -306,17 +328,20 @@ impl AuditClient {
|
||||
let event_kind = event.kind;
|
||||
let event_pubkey = event.pubkey;
|
||||
let event_id = event.id;
|
||||
|
||||
|
||||
let mut tags = vec![
|
||||
Tag::custom(TagKind::custom("E"), vec![event_id.to_hex(), "".to_string(), "root".to_string()]),
|
||||
Tag::custom(
|
||||
TagKind::custom("E"),
|
||||
vec![event_id.to_hex(), "".to_string(), "root".to_string()],
|
||||
),
|
||||
Tag::event(event_id),
|
||||
Tag::custom(TagKind::custom("K"), vec![event_kind.as_u16().to_string()]),
|
||||
Tag::public_key(event_pubkey),
|
||||
];
|
||||
|
||||
|
||||
// Add any additional tags
|
||||
tags.extend(additional_tags);
|
||||
|
||||
|
||||
self.event_builder(Kind::Custom(1111), content)
|
||||
.tags(tags)
|
||||
.build(self.keys())
|
||||
@@ -327,22 +352,22 @@ impl AuditClient {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client_creation() {
|
||||
let config = AuditConfig::ci();
|
||||
|
||||
|
||||
// This will fail if no relay is running, which is expected in tests
|
||||
// In real usage, there should be a relay at the URL
|
||||
let result = AuditClient::new("ws://localhost:7000", config).await;
|
||||
|
||||
|
||||
// We can't test connection without a running relay
|
||||
// But we can test that the client is created
|
||||
if let Ok(client) = result {
|
||||
assert_eq!(client.config.mode, AuditMode::CI);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_event_builder() {
|
||||
let config = AuditConfig::ci();
|
||||
@@ -352,13 +377,13 @@ mod tests {
|
||||
config: config.clone(),
|
||||
keys: keys.clone(),
|
||||
};
|
||||
|
||||
|
||||
let _builder = client.event_builder(Kind::TextNote, "test content");
|
||||
|
||||
|
||||
// Builder should be created successfully
|
||||
// (We can't test the internal config field as it's private, which is correct)
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_audit_tags_automatically_added() {
|
||||
let config = AuditConfig::ci();
|
||||
@@ -368,21 +393,28 @@ mod tests {
|
||||
config: config.clone(),
|
||||
keys: keys.clone(),
|
||||
};
|
||||
|
||||
|
||||
// Create an event with a custom tag
|
||||
let event = client.event_builder(Kind::TextNote, "test content")
|
||||
let event = client
|
||||
.event_builder(Kind::TextNote, "test content")
|
||||
.tag(Tag::custom(TagKind::custom("custom"), vec!["value"]))
|
||||
.build(&keys)
|
||||
.unwrap();
|
||||
|
||||
|
||||
// Should have custom tag (1) + 3 audit tags = at least 4 tags
|
||||
assert!(event.tags.len() >= 4, "Expected at least 4 tags, got {}", event.tags.len());
|
||||
|
||||
assert!(
|
||||
event.tags.len() >= 4,
|
||||
"Expected at least 4 tags, got {}",
|
||||
event.tags.len()
|
||||
);
|
||||
|
||||
// Verify audit tags are present by checking tag content
|
||||
let tag_contents: Vec<String> = event.tags.iter()
|
||||
let tag_contents: Vec<String> = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| t.content().map(|s| s.to_string()))
|
||||
.collect();
|
||||
|
||||
|
||||
// Check for the three required audit tags
|
||||
assert!(
|
||||
tag_contents.contains(&"grasp-audit-test-event".to_string()),
|
||||
@@ -393,10 +425,12 @@ mod tests {
|
||||
"Missing 'audit-ci-*' tag"
|
||||
);
|
||||
assert!(
|
||||
tag_contents.iter().any(|t| t.starts_with("audit-cleanup-after-")),
|
||||
tag_contents
|
||||
.iter()
|
||||
.any(|t| t.starts_with("audit-cleanup-after-")),
|
||||
"Missing 'audit-cleanup-after-*' tag"
|
||||
);
|
||||
|
||||
|
||||
// Verify the custom tag is also present
|
||||
assert!(
|
||||
tag_contents.contains(&"value".to_string()),
|
||||
|
||||
+95
-67
@@ -33,13 +33,13 @@ use std::sync::{Arc, Mutex};
|
||||
pub enum FixtureKind {
|
||||
/// Basic repository announcement (kind 30617)
|
||||
ValidRepo,
|
||||
|
||||
|
||||
/// Repository with one issue (kind 1621)
|
||||
RepoWithIssue,
|
||||
|
||||
|
||||
/// Repository with issue and comment (kind 1111)
|
||||
RepoWithComment,
|
||||
|
||||
|
||||
/// Repository state announcement (kind 30618)
|
||||
RepoState,
|
||||
}
|
||||
@@ -49,7 +49,7 @@ pub enum FixtureKind {
|
||||
pub enum ContextMode {
|
||||
/// Create fresh fixtures for each request (test isolation)
|
||||
Isolated,
|
||||
|
||||
|
||||
/// Reuse shared fixtures across requests (minimal events)
|
||||
Shared,
|
||||
}
|
||||
@@ -104,7 +104,7 @@ impl<'a> TestContext<'a> {
|
||||
cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a test context with explicit mode override
|
||||
///
|
||||
/// This is useful for testing the context itself or for advanced use cases
|
||||
@@ -116,7 +116,7 @@ impl<'a> TestContext<'a> {
|
||||
cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get a fixture, creating it if needed based on mode
|
||||
///
|
||||
/// # Behavior
|
||||
@@ -139,7 +139,7 @@ impl<'a> TestContext<'a> {
|
||||
ContextMode::Shared => self.get_or_create_shared(kind).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get the underlying client for direct access
|
||||
///
|
||||
/// This allows tests to use the client directly when needed while still
|
||||
@@ -147,23 +147,27 @@ impl<'a> TestContext<'a> {
|
||||
pub fn client(&self) -> &'a AuditClient {
|
||||
self.client
|
||||
}
|
||||
|
||||
|
||||
/// Get the current context mode
|
||||
pub fn mode(&self) -> ContextMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
|
||||
/// Create a fresh fixture (always creates new)
|
||||
async fn create_fresh(&self, kind: FixtureKind) -> Result<Event> {
|
||||
let event = self.build_fixture(kind).await
|
||||
let event = self
|
||||
.build_fixture(kind)
|
||||
.await
|
||||
.with_context(|| format!("Failed to build {:?} fixture", kind))?;
|
||||
|
||||
self.client.send_event(event.clone()).await
|
||||
|
||||
self.client
|
||||
.send_event(event.clone())
|
||||
.await
|
||||
.with_context(|| format!("Failed to send {:?} fixture event to relay", kind))?;
|
||||
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
|
||||
/// Get or create a shared fixture (caches for reuse)
|
||||
async fn get_or_create_shared(&self, kind: FixtureKind) -> Result<Event> {
|
||||
// Check cache first
|
||||
@@ -173,39 +177,54 @@ impl<'a> TestContext<'a> {
|
||||
return Ok(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Not in cache, create it
|
||||
let event = self.build_fixture(kind).await
|
||||
let event = self
|
||||
.build_fixture(kind)
|
||||
.await
|
||||
.with_context(|| format!("Failed to build {:?} fixture for shared cache", kind))?;
|
||||
|
||||
self.client.send_event(event.clone()).await
|
||||
.with_context(|| format!("Failed to send {:?} fixture event to relay (shared cache)", kind))?;
|
||||
|
||||
|
||||
self.client
|
||||
.send_event(event.clone())
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to send {:?} fixture event to relay (shared cache)",
|
||||
kind
|
||||
)
|
||||
})?;
|
||||
|
||||
// Store in cache
|
||||
{
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
cache.insert(kind, event.clone());
|
||||
}
|
||||
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
|
||||
/// Build a fixture event (doesn't send it)
|
||||
async fn build_fixture(&self, kind: FixtureKind) -> Result<Event> {
|
||||
match kind {
|
||||
FixtureKind::ValidRepo => {
|
||||
let test_name = format!("fixture-{:?}-{}", kind, &uuid::Uuid::new_v4().to_string()[..8]);
|
||||
let test_name = format!(
|
||||
"fixture-{:?}-{}",
|
||||
kind,
|
||||
&uuid::Uuid::new_v4().to_string()[..8]
|
||||
);
|
||||
self.client.create_repo_announcement(&test_name).await
|
||||
}
|
||||
|
||||
|
||||
FixtureKind::RepoWithIssue => {
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
// First create and send repo
|
||||
let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]);
|
||||
let test_name = format!(
|
||||
"fixture-{:?}-{}",
|
||||
FixtureKind::ValidRepo,
|
||||
&uuid::Uuid::new_v4().to_string()[..8]
|
||||
);
|
||||
let repo = self.client.create_repo_announcement(&test_name).await?;
|
||||
self.client.send_event(repo.clone()).await?;
|
||||
|
||||
|
||||
// Then create issue referencing it - this will have 'a' tag to repo
|
||||
// Note: We build the issue but DON'T send it here - the caller will send it
|
||||
let issue = self.client.create_issue(
|
||||
@@ -214,64 +233,70 @@ impl<'a> TestContext<'a> {
|
||||
"Issue content for testing",
|
||||
vec![],
|
||||
)?;
|
||||
|
||||
|
||||
// Return the issue - tests can extract repo reference from its 'a' tag
|
||||
// The caller (create_fresh/get_or_create_shared) will send this event
|
||||
Ok(issue)
|
||||
}
|
||||
|
||||
|
||||
FixtureKind::RepoWithComment => {
|
||||
// First create repo with issue
|
||||
let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]);
|
||||
let test_name = format!(
|
||||
"fixture-{:?}-{}",
|
||||
FixtureKind::ValidRepo,
|
||||
&uuid::Uuid::new_v4().to_string()[..8]
|
||||
);
|
||||
let repo = self.client.create_repo_announcement(&test_name).await?;
|
||||
self.client.send_event(repo.clone()).await?;
|
||||
|
||||
let issue = self.client.create_issue(
|
||||
&repo,
|
||||
"Test Issue",
|
||||
"Issue content",
|
||||
vec![],
|
||||
)?;
|
||||
|
||||
let issue =
|
||||
self.client
|
||||
.create_issue(&repo, "Test Issue", "Issue content", vec![])?;
|
||||
self.client.send_event(issue.clone()).await?;
|
||||
|
||||
|
||||
// Then create comment on issue
|
||||
self.client.create_comment(
|
||||
&issue,
|
||||
"Test comment",
|
||||
vec![],
|
||||
)
|
||||
self.client.create_comment(&issue, "Test comment", vec![])
|
||||
}
|
||||
|
||||
|
||||
FixtureKind::RepoState => {
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
|
||||
// First create repo announcement
|
||||
let test_name = format!("fixture-{:?}-{}", FixtureKind::ValidRepo, &uuid::Uuid::new_v4().to_string()[..8]);
|
||||
let test_name = format!(
|
||||
"fixture-{:?}-{}",
|
||||
FixtureKind::ValidRepo,
|
||||
&uuid::Uuid::new_v4().to_string()[..8]
|
||||
);
|
||||
let repo = self.client.create_repo_announcement(&test_name).await?;
|
||||
self.client.send_event(repo.clone()).await?;
|
||||
|
||||
|
||||
// Extract repo_id from repo announcement
|
||||
let repo_id = repo.tags.iter()
|
||||
let repo_id = repo
|
||||
.tags
|
||||
.iter()
|
||||
.find(|t| t.kind() == TagKind::d())
|
||||
.and_then(|t| t.content())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing d tag in repo announcement"))?
|
||||
.to_string();
|
||||
|
||||
|
||||
// Create state announcement
|
||||
self.client.event_builder(Kind::Custom(30618), "")
|
||||
self.client
|
||||
.event_builder(Kind::Custom(30618), "")
|
||||
.tag(Tag::identifier(&repo_id))
|
||||
.tag(Tag::custom(TagKind::custom("refs/heads/main"), vec![
|
||||
"abc123def456789012345678901234567890abcd"
|
||||
]))
|
||||
.tag(Tag::custom(TagKind::custom("HEAD"), vec![
|
||||
"ref: refs/heads/main"
|
||||
]))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("refs/heads/main"),
|
||||
vec!["abc123def456789012345678901234567890abcd"],
|
||||
))
|
||||
.tag(Tag::custom(
|
||||
TagKind::custom("HEAD"),
|
||||
vec!["ref: refs/heads/main"],
|
||||
))
|
||||
.build(self.client.keys())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to build state announcement: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Clear the fixture cache
|
||||
///
|
||||
/// This is useful for tests that want to ensure fresh fixtures
|
||||
@@ -286,34 +311,37 @@ impl<'a> TestContext<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AuditConfig;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_context_mode_from_audit_mode() {
|
||||
assert_eq!(ContextMode::from(AuditMode::CI), ContextMode::Isolated);
|
||||
assert_eq!(ContextMode::from(AuditMode::Production), ContextMode::Shared);
|
||||
assert_eq!(
|
||||
ContextMode::from(AuditMode::Production),
|
||||
ContextMode::Shared
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_fixture_kind_hash() {
|
||||
use std::collections::HashSet;
|
||||
|
||||
|
||||
let mut set = HashSet::new();
|
||||
set.insert(FixtureKind::ValidRepo);
|
||||
set.insert(FixtureKind::RepoWithIssue);
|
||||
|
||||
|
||||
assert!(set.contains(&FixtureKind::ValidRepo));
|
||||
assert!(!set.contains(&FixtureKind::RepoWithComment));
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_context_creation() {
|
||||
let config = AuditConfig::ci();
|
||||
let client = crate::AuditClient::new_test(config);
|
||||
|
||||
|
||||
let ctx = TestContext::new(&client);
|
||||
assert_eq!(ctx.mode(), ContextMode::Isolated);
|
||||
|
||||
|
||||
let ctx = TestContext::with_mode(&client, ContextMode::Shared);
|
||||
assert_eq!(ctx.mode(), ContextMode::Shared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub fn generate_test_id() -> String {
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
|
||||
format!("test-{}-{}", timestamp, counter)
|
||||
}
|
||||
|
||||
@@ -26,29 +26,29 @@ pub fn generate_prod_run_id() -> String {
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
|
||||
format!("prod-audit-{}", timestamp)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_generate_test_id() {
|
||||
let id1 = generate_test_id();
|
||||
let id2 = generate_test_id();
|
||||
|
||||
|
||||
assert_ne!(id1, id2);
|
||||
assert!(id1.starts_with("test-"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_generate_ci_run_id() {
|
||||
let id = generate_ci_run_id();
|
||||
assert!(id.starts_with("ci-"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_generate_prod_run_id() {
|
||||
let id = generate_prod_run_id();
|
||||
|
||||
@@ -29,15 +29,15 @@
|
||||
//! ```
|
||||
|
||||
pub mod audit;
|
||||
pub mod fixtures;
|
||||
pub mod client;
|
||||
pub mod fixtures;
|
||||
pub mod isolation;
|
||||
pub mod result;
|
||||
pub mod specs;
|
||||
|
||||
pub use audit::{AuditConfig, AuditMode};
|
||||
pub use fixtures::{ContextMode, FixtureKind, TestContext};
|
||||
pub use client::AuditClient;
|
||||
pub use fixtures::{ContextMode, FixtureKind, TestContext};
|
||||
pub use result::{AuditResult, TestResult};
|
||||
|
||||
// Re-export commonly used types
|
||||
|
||||
+29
-28
@@ -25,7 +25,7 @@ impl TestResult {
|
||||
duration: Duration::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Run a test function and capture the result
|
||||
pub async fn run<F, Fut>(mut self, test_fn: F) -> Self
|
||||
where
|
||||
@@ -33,7 +33,7 @@ impl TestResult {
|
||||
Fut: std::future::Future<Output = Result<(), String>>,
|
||||
{
|
||||
let start = Instant::now();
|
||||
|
||||
|
||||
match test_fn().await {
|
||||
Ok(()) => {
|
||||
self.passed = true;
|
||||
@@ -43,17 +43,17 @@ impl TestResult {
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.duration = start.elapsed();
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Mark test as passed
|
||||
pub fn pass(mut self) -> Self {
|
||||
self.passed = true;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Mark test as failed with error
|
||||
pub fn fail(mut self, error: impl Into<String>) -> Self {
|
||||
self.passed = false;
|
||||
@@ -77,68 +77,69 @@ impl AuditResult {
|
||||
results: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add a test result
|
||||
pub fn add(&mut self, result: TestResult) {
|
||||
self.results.push(result);
|
||||
}
|
||||
|
||||
|
||||
/// Merge another audit result
|
||||
pub fn merge(&mut self, other: AuditResult) {
|
||||
self.results.extend(other.results);
|
||||
}
|
||||
|
||||
|
||||
/// Check if all tests passed
|
||||
pub fn all_passed(&self) -> bool {
|
||||
self.results.iter().all(|r| r.passed)
|
||||
}
|
||||
|
||||
|
||||
/// Get count of passed tests
|
||||
pub fn passed_count(&self) -> usize {
|
||||
self.results.iter().filter(|r| r.passed).count()
|
||||
}
|
||||
|
||||
|
||||
/// Get count of failed tests
|
||||
pub fn failed_count(&self) -> usize {
|
||||
self.results.iter().filter(|r| !r.passed).count()
|
||||
}
|
||||
|
||||
|
||||
/// Get total count of tests
|
||||
pub fn total_count(&self) -> usize {
|
||||
self.results.len()
|
||||
}
|
||||
|
||||
|
||||
/// Print a detailed report
|
||||
pub fn print_report(&self) {
|
||||
println!("\n{}", self.spec);
|
||||
println!("{}", "═".repeat(60));
|
||||
println!();
|
||||
|
||||
|
||||
let passed = self.passed_count();
|
||||
let total = self.total_count();
|
||||
|
||||
|
||||
for result in &self.results {
|
||||
let status = if result.passed { "✓" } else { "✗" };
|
||||
|
||||
|
||||
println!("{} {} ({})", status, result.name, result.spec_ref);
|
||||
println!(" Requirement: {}", result.requirement);
|
||||
|
||||
|
||||
if let Some(error) = &result.error {
|
||||
println!(" Error: {}", error);
|
||||
}
|
||||
|
||||
|
||||
println!(" Duration: {:?}", result.duration);
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("Results: {}/{} passed ({:.1}%)",
|
||||
passed,
|
||||
|
||||
println!(
|
||||
"Results: {}/{} passed ({:.1}%)",
|
||||
passed,
|
||||
total,
|
||||
(passed as f64 / total as f64) * 100.0
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
|
||||
/// Get a summary string
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
@@ -153,34 +154,34 @@ impl AuditResult {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_pass() {
|
||||
let result = TestResult::new("test", "SPEC:1", "Must work")
|
||||
.run(|| async { Ok(()) })
|
||||
.await;
|
||||
|
||||
|
||||
assert!(result.passed);
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_result_fail() {
|
||||
let result = TestResult::new("test", "SPEC:1", "Must work")
|
||||
.run(|| async { Err("Failed".to_string()) })
|
||||
.await;
|
||||
|
||||
|
||||
assert!(!result.passed);
|
||||
assert_eq!(result.error, Some("Failed".to_string()));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_audit_result() {
|
||||
let mut audit = AuditResult::new("Test Spec");
|
||||
|
||||
|
||||
audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass());
|
||||
audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error"));
|
||||
|
||||
|
||||
assert_eq!(audit.total_count(), 2);
|
||||
assert_eq!(audit.passed_count(), 1);
|
||||
assert_eq!(audit.failed_count(), 1);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,4 +6,4 @@ pub mod nip11_document;
|
||||
|
||||
pub use event_acceptance_policy::EventAcceptancePolicyTests;
|
||||
pub use nip01_smoke::Nip01SmokeTests;
|
||||
pub use nip11_document::Nip11DocumentTests;
|
||||
pub use nip11_document::Nip11DocumentTests;
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Nip01SmokeTests {
|
||||
/// Run all NIP-01 smoke tests
|
||||
pub async fn run_all(client: &AuditClient) -> AuditResult {
|
||||
let mut results = AuditResult::new("NIP-01 Smoke Tests");
|
||||
|
||||
|
||||
// Run tests sequentially to avoid future type issues
|
||||
results.add(Self::test_websocket_connection(client).await);
|
||||
results.add(Self::test_send_receive_event(client).await);
|
||||
@@ -21,10 +21,10 @@ impl Nip01SmokeTests {
|
||||
results.add(Self::test_close_subscription(client).await);
|
||||
results.add(Self::test_reject_invalid_signature(client).await);
|
||||
results.add(Self::test_reject_invalid_event_id(client).await);
|
||||
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
|
||||
/// Test 1: Can establish WebSocket connection
|
||||
///
|
||||
/// Spec: NIP-01 basic requirement
|
||||
@@ -39,17 +39,17 @@ impl Nip01SmokeTests {
|
||||
if !client.is_connected().await {
|
||||
return Err("Failed to connect to relay".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test 2: Can send EVENT and receive OK response
|
||||
///
|
||||
/// Spec: NIP-01 EVENT message
|
||||
/// Requirement: Relay MUST accept valid EVENT messages
|
||||
///
|
||||
///
|
||||
/// For GRASP servers, we send a NIP-34 repository announcement that lists
|
||||
/// the GRASP server in clone and relays tags (required for acceptance).
|
||||
async fn test_send_receive_event(client: &AuditClient) -> TestResult {
|
||||
@@ -60,15 +60,17 @@ impl Nip01SmokeTests {
|
||||
)
|
||||
.run(|| async {
|
||||
// Create a NIP-34 announcement event
|
||||
let event = client.create_repo_announcement("send_receive_event").await
|
||||
let event = client
|
||||
.create_repo_announcement("send_receive_event")
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create announcement: {}", e))?;
|
||||
|
||||
|
||||
// Send event
|
||||
let event_id = client
|
||||
.send_event(event.clone())
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send event: {}", e))?;
|
||||
|
||||
|
||||
// Verify we got an event ID back
|
||||
if event_id != event.id {
|
||||
return Err(format!(
|
||||
@@ -76,43 +78,47 @@ impl Nip01SmokeTests {
|
||||
event.id, event_id
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Wait a bit for event to be indexed
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
|
||||
// Try to query it back
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(30617))
|
||||
.id(event_id);
|
||||
|
||||
let filter = Filter::new().kind(Kind::Custom(30617)).id(event_id);
|
||||
|
||||
let events = client
|
||||
.query(filter)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to query event: {}", e))?;
|
||||
|
||||
|
||||
if events.is_empty() {
|
||||
// Debug: try querying without audit client filtering
|
||||
eprintln!("Event not found with audit client query, trying direct client query...");
|
||||
let direct_filter = Filter::new().kind(Kind::Custom(30617)).id(event_id);
|
||||
let direct_events = client.client().fetch_events(direct_filter, std::time::Duration::from_secs(5)).await
|
||||
let direct_events = client
|
||||
.client()
|
||||
.fetch_events(direct_filter, std::time::Duration::from_secs(5))
|
||||
.await
|
||||
.map_err(|e| format!("Direct query failed: {}", e))?;
|
||||
let direct_vec: Vec<Event> = direct_events.into_iter().collect();
|
||||
eprintln!("Direct query found {} events", direct_vec.len());
|
||||
if !direct_vec.is_empty() {
|
||||
eprintln!("Event tags: {:?}", direct_vec[0].tags);
|
||||
}
|
||||
return Err(format!("Event not found after sending (direct query found {})", direct_vec.len()));
|
||||
return Err(format!(
|
||||
"Event not found after sending (direct query found {})",
|
||||
direct_vec.len()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
if events[0].id != event_id {
|
||||
return Err("Retrieved event has different ID".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test 3: Can create subscription with REQ
|
||||
///
|
||||
/// Spec: NIP-01 REQ message
|
||||
@@ -125,34 +131,36 @@ impl Nip01SmokeTests {
|
||||
)
|
||||
.run(|| async {
|
||||
// Create a NIP-34 announcement event (accepted by GRASP relays)
|
||||
let event = client.create_repo_announcement("create_subscription").await
|
||||
let event = client
|
||||
.create_repo_announcement("create_subscription")
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create announcement: {}", e))?;
|
||||
|
||||
let event_id = client
|
||||
|
||||
let _event_id = client
|
||||
.send_event(event.clone())
|
||||
.await
|
||||
.map_err(|e| format!("Failed to send event: {}", e))?;
|
||||
|
||||
|
||||
// Subscribe to NIP-34 announcements from this author
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(30617))
|
||||
.author(client.public_key());
|
||||
|
||||
|
||||
let events = client
|
||||
.subscribe(vec![filter], Some(std::time::Duration::from_secs(5)))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to subscribe: {}", e))?;
|
||||
|
||||
|
||||
// Should have at least our event
|
||||
if events.is_empty() {
|
||||
return Err("No events received from subscription".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test 4: Can close subscription with CLOSE
|
||||
///
|
||||
/// Spec: NIP-01 CLOSE message
|
||||
@@ -167,22 +175,20 @@ impl Nip01SmokeTests {
|
||||
// For now, we just verify we can query events
|
||||
// Full subscription management with CLOSE would require
|
||||
// lower-level WebSocket access
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::TextNote)
|
||||
.limit(1);
|
||||
|
||||
|
||||
let filter = Filter::new().kind(Kind::TextNote).limit(1);
|
||||
|
||||
let _events = client
|
||||
.subscribe(vec![filter], Some(std::time::Duration::from_secs(2)))
|
||||
.await
|
||||
.map_err(|e| format!("Failed to subscribe: {}", e))?;
|
||||
|
||||
|
||||
// If we got here, subscription worked
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test 5: Rejects events with invalid signatures
|
||||
///
|
||||
/// Spec: NIP-01 event validation
|
||||
@@ -199,7 +205,7 @@ impl Nip01SmokeTests {
|
||||
.event_builder(Kind::TextNote, "Invalid signature test")
|
||||
.build(client.keys())
|
||||
.map_err(|e| format!("Failed to build event: {}", e))?;
|
||||
|
||||
|
||||
// Corrupt the signature by creating a new event with wrong sig
|
||||
// We'll use a different key to sign, creating an invalid signature
|
||||
let wrong_keys = Keys::generate();
|
||||
@@ -207,7 +213,7 @@ impl Nip01SmokeTests {
|
||||
.tags(event.tags.clone())
|
||||
.sign_with_keys(&wrong_keys)
|
||||
.map_err(|e| format!("Failed to build wrong event: {}", e))?;
|
||||
|
||||
|
||||
// Create event JSON with mismatched pubkey and signature
|
||||
// This should be rejected by the relay
|
||||
let invalid_event_json = serde_json::json!({
|
||||
@@ -219,24 +225,24 @@ impl Nip01SmokeTests {
|
||||
"content": event.content,
|
||||
"sig": wrong_event.sig.to_string(), // Wrong signature!
|
||||
});
|
||||
|
||||
|
||||
// Parse it back to an Event
|
||||
let invalid_event: Event = serde_json::from_value(invalid_event_json)
|
||||
.map_err(|e| format!("Failed to create invalid event: {}", e))?;
|
||||
|
||||
|
||||
// Try to send the invalid event
|
||||
let result = client.send_event(invalid_event).await;
|
||||
|
||||
|
||||
// We expect this to fail
|
||||
if result.is_ok() {
|
||||
return Err("Relay accepted event with invalid signature".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test 6: Rejects events with invalid event IDs
|
||||
///
|
||||
/// Spec: NIP-01 event ID validation
|
||||
@@ -253,7 +259,7 @@ impl Nip01SmokeTests {
|
||||
.event_builder(Kind::TextNote, "Invalid ID test")
|
||||
.build(client.keys())
|
||||
.map_err(|e| format!("Failed to build event: {}", e))?;
|
||||
|
||||
|
||||
// Create event JSON with corrupted ID
|
||||
let invalid_event_json = serde_json::json!({
|
||||
"id": EventId::all_zeros().to_hex(), // Wrong ID!
|
||||
@@ -264,19 +270,19 @@ impl Nip01SmokeTests {
|
||||
"content": event.content,
|
||||
"sig": event.sig.to_string(),
|
||||
});
|
||||
|
||||
|
||||
// Parse it back to an Event
|
||||
let invalid_event: Event = serde_json::from_value(invalid_event_json)
|
||||
.map_err(|e| format!("Failed to create invalid event: {}", e))?;
|
||||
|
||||
|
||||
// Try to send the invalid event
|
||||
let result = client.send_event(invalid_event).await;
|
||||
|
||||
|
||||
// We expect this to fail
|
||||
if result.is_ok() {
|
||||
return Err("Relay accepted event with invalid ID".to_string());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -287,25 +293,25 @@ impl Nip01SmokeTests {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AuditConfig;
|
||||
|
||||
|
||||
// Note: These tests require a running relay
|
||||
// They are integration tests, not unit tests
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Ignore by default since it needs a running relay
|
||||
async fn test_smoke_tests_against_relay() {
|
||||
// RELAY_URL env var must be set - no default fallback
|
||||
let relay_url = std::env::var("RELAY_URL")
|
||||
.expect("RELAY_URL environment variable must be set for integration tests");
|
||||
|
||||
|
||||
let config = AuditConfig::ci();
|
||||
let client = AuditClient::new(&relay_url, config)
|
||||
.await
|
||||
.expect("Failed to connect to relay");
|
||||
|
||||
|
||||
let results = Nip01SmokeTests::run_all(&client).await;
|
||||
results.print_report();
|
||||
|
||||
|
||||
assert!(results.all_passed(), "Some smoke tests failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
//! - Handles curation field correctly (present if curated, absent otherwise)
|
||||
|
||||
use crate::{AuditClient, AuditResult, TestResult};
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
pub struct Nip11DocumentTests;
|
||||
|
||||
@@ -17,25 +16,25 @@ impl Nip11DocumentTests {
|
||||
/// Run all NIP-11 document tests
|
||||
pub async fn run_all(client: &AuditClient) -> AuditResult {
|
||||
let mut results = AuditResult::new("GRASP-01 NIP-11 Document Tests");
|
||||
|
||||
|
||||
// NIP-11 relay information tests
|
||||
results.add(Self::test_nip11_document_exists(client).await);
|
||||
results.add(Self::test_nip11_supported_grasps_field(client).await);
|
||||
results.add(Self::test_nip11_repo_acceptance_criteria_field(client).await);
|
||||
results.add(Self::test_nip11_curation_field(client).await);
|
||||
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// NIP-11 Relay Information Tests
|
||||
// =========================================================================
|
||||
|
||||
|
||||
/// Test: Serve NIP-11 document
|
||||
///
|
||||
/// Spec: Line 11 of ../grasp/01.md
|
||||
/// Requirement: MUST serve NIP-11 document
|
||||
async fn test_nip11_document_exists(client: &AuditClient) -> TestResult {
|
||||
async fn test_nip11_document_exists(_client: &AuditClient) -> TestResult {
|
||||
TestResult::new(
|
||||
"nip11_document_exists",
|
||||
"GRASP-01:nostr-relay:11",
|
||||
@@ -52,17 +51,17 @@ impl Nip11DocumentTests {
|
||||
// 4. Verify response is valid JSON
|
||||
// 5. Parse as NIP-11 document
|
||||
// 6. Verify has required fields (name, description, etc.)
|
||||
|
||||
|
||||
Err("Not implemented yet".to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test: NIP-11 includes supported_grasps field
|
||||
///
|
||||
/// Spec: Line 12 of ../grasp/01.md
|
||||
/// Requirement: MUST list supported GRASPs as string array
|
||||
async fn test_nip11_supported_grasps_field(client: &AuditClient) -> TestResult {
|
||||
async fn test_nip11_supported_grasps_field(_client: &AuditClient) -> TestResult {
|
||||
TestResult::new(
|
||||
"nip11_supported_grasps_field",
|
||||
"GRASP-01:nostr-relay:12",
|
||||
@@ -76,17 +75,17 @@ impl Nip11DocumentTests {
|
||||
// 4. Verify array includes "GRASP-01"
|
||||
// 5. Verify format: each entry matches pattern "GRASP-\d{2}"
|
||||
// 6. Document other GRASPs found (for info)
|
||||
|
||||
|
||||
Err("Not implemented yet".to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test: NIP-11 includes repo_acceptance_criteria field
|
||||
///
|
||||
/// Spec: Line 13 of ../grasp/01.md
|
||||
/// Requirement: MUST list repository acceptance criteria
|
||||
async fn test_nip11_repo_acceptance_criteria_field(client: &AuditClient) -> TestResult {
|
||||
async fn test_nip11_repo_acceptance_criteria_field(_client: &AuditClient) -> TestResult {
|
||||
TestResult::new(
|
||||
"nip11_repo_acceptance_criteria_field",
|
||||
"GRASP-01:nostr-relay:13",
|
||||
@@ -101,17 +100,17 @@ impl Nip11DocumentTests {
|
||||
// 5. Document the criteria (for info)
|
||||
// Examples: "Must list this relay in clone and relays tags"
|
||||
// "Pre-payment required via Lightning invoice"
|
||||
|
||||
|
||||
Err("Not implemented yet".to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Test: NIP-11 curation field handling
|
||||
///
|
||||
/// Spec: Line 14 of ../grasp/01.md
|
||||
/// Requirement: MUST include curation if curated, omit otherwise
|
||||
async fn test_nip11_curation_field(client: &AuditClient) -> TestResult {
|
||||
async fn test_nip11_curation_field(_client: &AuditClient) -> TestResult {
|
||||
TestResult::new(
|
||||
"nip11_curation_field",
|
||||
"GRASP-01:nostr-relay:14",
|
||||
@@ -127,39 +126,41 @@ impl Nip11DocumentTests {
|
||||
// 4. If absent:
|
||||
// - Document that no curation beyond SPAM prevention
|
||||
// 5. Both cases are valid per spec
|
||||
|
||||
|
||||
Err("Not implemented yet".to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AuditConfig;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires running relay
|
||||
async fn test_grasp01_nip11_document_against_relay() {
|
||||
// Read relay URL from environment variable - must be supplied
|
||||
let relay_url = std::env::var("RELAY_URL")
|
||||
.expect("RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081");
|
||||
|
||||
let config = AuditConfig::ci();
|
||||
let client = AuditClient::new(&relay_url, config)
|
||||
.await
|
||||
.expect(&format!(
|
||||
"Failed to connect to relay at {}. Ensure relay is running and accessible. \
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires running relay
|
||||
async fn test_grasp01_nip11_document_against_relay() {
|
||||
// Read relay URL from environment variable - must be supplied
|
||||
let relay_url = std::env::var("RELAY_URL").expect(
|
||||
"RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081",
|
||||
);
|
||||
|
||||
let config = AuditConfig::ci();
|
||||
let client = AuditClient::new(&relay_url, config)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Failed to connect to relay at {}. Ensure relay is running and accessible. \
|
||||
Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest",
|
||||
relay_url
|
||||
));
|
||||
|
||||
relay_url
|
||||
)
|
||||
});
|
||||
|
||||
let results = Nip11DocumentTests::run_all(&client).await;
|
||||
results.print_report();
|
||||
|
||||
|
||||
// Don't assert all passed yet - tests not implemented
|
||||
// assert!(results.all_passed(), "Some GRASP-01 NIP-11 document tests failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,4 @@
|
||||
pub mod grasp01;
|
||||
|
||||
// Re-export all test structs from grasp01 module
|
||||
pub use grasp01::{
|
||||
EventAcceptancePolicyTests,
|
||||
Nip01SmokeTests,
|
||||
Nip11DocumentTests,
|
||||
};
|
||||
pub use grasp01::{EventAcceptancePolicyTests, Nip01SmokeTests, Nip11DocumentTests};
|
||||
|
||||
+2
-4
@@ -19,10 +19,8 @@ impl Config {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
Ok(Config {
|
||||
domain: env::var("NGIT_DOMAIN")
|
||||
.unwrap_or_else(|_| "localhost:8080".to_string()),
|
||||
owner_npub: env::var("NGIT_OWNER_NPUB")
|
||||
.context("NGIT_OWNER_NPUB must be set")?,
|
||||
domain: env::var("NGIT_DOMAIN").unwrap_or_else(|_| "localhost:8080".to_string()),
|
||||
owner_npub: env::var("NGIT_OWNER_NPUB").context("NGIT_OWNER_NPUB must be set")?,
|
||||
relay_name: env::var("NGIT_RELAY_NAME")
|
||||
.unwrap_or_else(|_| "ngit-grasp relay".to_string()),
|
||||
relay_description: env::var("NGIT_RELAY_DESCRIPTION")
|
||||
|
||||
+9
-6
@@ -1,7 +1,6 @@
|
||||
/// Landing Page Handler
|
||||
///
|
||||
///
|
||||
/// Serves the HTML landing page or upgrades to WebSocket for Nostr relay connections.
|
||||
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result};
|
||||
use nostr_relay_builder::LocalRelay;
|
||||
|
||||
@@ -16,12 +15,16 @@ pub async fn handle(
|
||||
) -> Result<HttpResponse> {
|
||||
// Check if this is a WebSocket upgrade request
|
||||
if let Some(upgrade) = req.headers().get("upgrade") {
|
||||
if upgrade.to_str().unwrap_or("").eq_ignore_ascii_case("websocket") {
|
||||
if upgrade
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.eq_ignore_ascii_case("websocket")
|
||||
{
|
||||
// Delegate to WebSocket handler
|
||||
return crate::http::websocket::handle(req, stream, relay).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, serve the landing page
|
||||
let html = format!(
|
||||
include_str!("../../templates/landing.html"),
|
||||
@@ -30,8 +33,8 @@ pub async fn handle(
|
||||
domain = config.domain,
|
||||
bind_address = config.bind_address,
|
||||
);
|
||||
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html))
|
||||
}
|
||||
}
|
||||
|
||||
+5
-6
@@ -1,7 +1,6 @@
|
||||
/// HTTP Server Module
|
||||
///
|
||||
///
|
||||
/// Provides actix-web HTTP server with WebSocket upgrade support for the Nostr relay.
|
||||
|
||||
pub mod landing;
|
||||
pub mod websocket;
|
||||
|
||||
@@ -13,9 +12,9 @@ use crate::config::Config;
|
||||
/// Start the HTTP server with integrated Nostr relay
|
||||
pub async fn run_server(config: Config, relay: LocalRelay) -> anyhow::Result<()> {
|
||||
let bind_addr = config.bind_address.clone();
|
||||
|
||||
|
||||
tracing::info!("Starting HTTP server on {}", bind_addr);
|
||||
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(config.clone()))
|
||||
@@ -26,6 +25,6 @@ pub async fn run_server(config: Config, relay: LocalRelay) -> anyhow::Result<()>
|
||||
.bind(&bind_addr)?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,8 +1,7 @@
|
||||
/// WebSocket Handler
|
||||
///
|
||||
///
|
||||
/// Handles WebSocket upgrade requests and passes connections to the Nostr relay.
|
||||
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result, Error};
|
||||
use actix_web::{web, Error, HttpRequest, HttpResponse, Result};
|
||||
use actix_ws::Message;
|
||||
use futures_util::StreamExt;
|
||||
use nostr_relay_builder::LocalRelay;
|
||||
@@ -14,19 +13,20 @@ pub async fn handle(
|
||||
relay: web::Data<LocalRelay>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
|
||||
|
||||
let peer_addr = req.peer_addr()
|
||||
|
||||
let peer_addr = req
|
||||
.peer_addr()
|
||||
.unwrap_or_else(|| "0.0.0.0:0".parse().unwrap());
|
||||
|
||||
|
||||
tracing::debug!("WebSocket connection from {}", peer_addr);
|
||||
|
||||
|
||||
// Spawn task to handle the WebSocket connection
|
||||
// TODO: Will use relay.take_connection() for full Nostr relay integration
|
||||
let _relay = relay.get_ref().clone();
|
||||
actix_web::rt::spawn(async move {
|
||||
// Create a channel to communicate between actix-ws and relay
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
|
||||
// Spawn task to send messages from relay to client
|
||||
let mut session_clone = session.clone();
|
||||
actix_web::rt::spawn(async move {
|
||||
@@ -36,7 +36,7 @@ pub async fn handle(
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Handle incoming messages from client
|
||||
while let Some(Ok(msg)) = msg_stream.next().await {
|
||||
match msg {
|
||||
@@ -65,9 +65,9 @@ pub async fn handle(
|
||||
Message::Nop => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tracing::debug!("WebSocket connection closed for {}", peer_addr);
|
||||
});
|
||||
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
+29
-45
@@ -1,12 +1,11 @@
|
||||
/// NIP-34 Git Repository Event Handling
|
||||
///
|
||||
/// This module handles Git repository announcements (kind 30617) and
|
||||
///
|
||||
/// This module handles Git repository announcements (kind 30617) and
|
||||
/// repository state announcements (kind 30618) according to NIP-34 and GRASP-01.
|
||||
///
|
||||
/// Reference:
|
||||
/// - NIP-34: https://nips.nostr.com/34
|
||||
/// - GRASP-01: https://gitworkshop.dev/danconwaydev.com/grasp/01.md
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use nostr_sdk::{Event, Kind, TagKind, ToBech32};
|
||||
|
||||
@@ -138,8 +137,8 @@ impl RepositoryAnnouncement {
|
||||
}
|
||||
|
||||
/// Check if this announcement lists the service (both clone and relay)
|
||||
///
|
||||
/// GRASP-01 requirement: MUST reject announcements that do not list
|
||||
///
|
||||
/// GRASP-01 requirement: MUST reject announcements that do not list
|
||||
/// the service in both `clone` and `relays` tags unless implementing GRASP-05.
|
||||
pub fn lists_service(&self, domain: &str) -> bool {
|
||||
self.has_clone_url(domain) && self.has_relay(domain)
|
||||
@@ -278,19 +277,19 @@ impl RepositoryState {
|
||||
}
|
||||
|
||||
/// Validate a repository announcement according to GRASP-01
|
||||
///
|
||||
///
|
||||
/// Returns Ok(()) if valid, Err with reason if invalid.
|
||||
pub fn validate_announcement(event: &Event, domain: &str) -> Result<()> {
|
||||
// Must be kind 30617
|
||||
if event.kind != Kind::from(KIND_REPOSITORY_ANNOUNCEMENT) {
|
||||
return Err(anyhow!("Invalid kind: expected {}", KIND_REPOSITORY_ANNOUNCEMENT));
|
||||
return Err(anyhow!(
|
||||
"Invalid kind: expected {}",
|
||||
KIND_REPOSITORY_ANNOUNCEMENT
|
||||
));
|
||||
}
|
||||
|
||||
// Must have identifier
|
||||
let has_identifier = event
|
||||
.tags
|
||||
.iter()
|
||||
.any(|t| t.kind() == TagKind::d());
|
||||
let has_identifier = event.tags.iter().any(|t| t.kind() == TagKind::d());
|
||||
if !has_identifier {
|
||||
return Err(anyhow!("Missing required 'd' tag (identifier)"));
|
||||
}
|
||||
@@ -298,7 +297,7 @@ pub fn validate_announcement(event: &Event, domain: &str) -> Result<()> {
|
||||
// Parse full announcement to validate structure
|
||||
let announcement = RepositoryAnnouncement::from_event(event.clone())?;
|
||||
|
||||
// GRASP-01: MUST reject announcements that do not list the service
|
||||
// GRASP-01: MUST reject announcements that do not list the service
|
||||
// in both `clone` and `relays` tags unless implementing GRASP-05
|
||||
if !announcement.lists_service(domain) {
|
||||
return Err(anyhow!(
|
||||
@@ -313,7 +312,7 @@ pub fn validate_announcement(event: &Event, domain: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Validate a repository state announcement according to GRASP-01
|
||||
///
|
||||
///
|
||||
/// Returns Ok(()) if valid, Err with reason if invalid.
|
||||
pub fn validate_state(event: &Event) -> Result<()> {
|
||||
// Must be kind 30618
|
||||
@@ -322,10 +321,7 @@ pub fn validate_state(event: &Event) -> Result<()> {
|
||||
}
|
||||
|
||||
// Must have identifier
|
||||
let has_identifier = event
|
||||
.tags
|
||||
.iter()
|
||||
.any(|t| t.kind() == TagKind::d());
|
||||
let has_identifier = event.tags.iter().any(|t| t.kind() == TagKind::d());
|
||||
if !has_identifier {
|
||||
return Err(anyhow!("Missing required 'd' tag (identifier)"));
|
||||
}
|
||||
@@ -352,7 +348,7 @@ mod tests {
|
||||
relays: Vec<&str>,
|
||||
) -> Event {
|
||||
use nostr_sdk::Tag;
|
||||
|
||||
|
||||
let mut tags = vec![Tag::custom(
|
||||
nostr_sdk::TagKind::d(),
|
||||
vec![identifier.to_string()],
|
||||
@@ -372,18 +368,15 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
EventBuilder::new(
|
||||
Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
|
||||
"Test repository",
|
||||
)
|
||||
.tags(tags)
|
||||
.sign_with_keys(keys)
|
||||
.unwrap()
|
||||
EventBuilder::new(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT), "Test repository")
|
||||
.tags(tags)
|
||||
.sign_with_keys(keys)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn create_state_event(keys: &Keys, identifier: &str, branches: Vec<(&str, &str)>) -> Event {
|
||||
use nostr_sdk::Tag;
|
||||
|
||||
|
||||
let mut tags = vec![Tag::custom(
|
||||
nostr_sdk::TagKind::d(),
|
||||
vec![identifier.to_string()],
|
||||
@@ -392,10 +385,7 @@ mod tests {
|
||||
for (branch, commit) in branches {
|
||||
tags.push(Tag::custom(
|
||||
nostr_sdk::TagKind::Custom("ref".into()),
|
||||
vec![
|
||||
format!("refs/heads/{}", branch),
|
||||
commit.to_string(),
|
||||
],
|
||||
vec![format!("refs/heads/{}", branch), commit.to_string()],
|
||||
));
|
||||
}
|
||||
|
||||
@@ -428,12 +418,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_announcement_missing_identifier() {
|
||||
let keys = create_test_keys();
|
||||
let event = EventBuilder::new(
|
||||
Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
|
||||
"Test repository",
|
||||
)
|
||||
.sign_with_keys(&keys)
|
||||
.unwrap();
|
||||
let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT), "Test repository")
|
||||
.sign_with_keys(&keys)
|
||||
.unwrap();
|
||||
|
||||
let result = RepositoryAnnouncement::from_event(event);
|
||||
assert!(result.is_err());
|
||||
@@ -539,7 +526,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_announcement_maintainers() {
|
||||
use nostr_sdk::Tag;
|
||||
|
||||
|
||||
let keys = create_test_keys();
|
||||
let maintainer_keys = create_test_keys();
|
||||
|
||||
@@ -558,13 +545,10 @@ mod tests {
|
||||
// Add maintainer
|
||||
tags.push(Tag::public_key(maintainer_keys.public_key()));
|
||||
|
||||
let event = EventBuilder::new(
|
||||
Kind::from(KIND_REPOSITORY_ANNOUNCEMENT),
|
||||
"Test repository",
|
||||
)
|
||||
.tags(tags)
|
||||
.sign_with_keys(&keys)
|
||||
.unwrap();
|
||||
let event = EventBuilder::new(Kind::from(KIND_REPOSITORY_ANNOUNCEMENT), "Test repository")
|
||||
.tags(tags)
|
||||
.sign_with_keys(&keys)
|
||||
.unwrap();
|
||||
|
||||
let announcement = RepositoryAnnouncement::from_event(event).unwrap();
|
||||
assert_eq!(announcement.maintainers.len(), 1);
|
||||
@@ -573,7 +557,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_state_with_tags() {
|
||||
use nostr_sdk::Tag;
|
||||
|
||||
|
||||
let keys = create_test_keys();
|
||||
let mut tags = vec![Tag::custom(
|
||||
nostr_sdk::TagKind::d(),
|
||||
|
||||
+8
-12
@@ -59,11 +59,7 @@ impl TestRelay {
|
||||
.spawn()
|
||||
.expect("Failed to start relay process");
|
||||
|
||||
let relay = Self {
|
||||
process,
|
||||
url,
|
||||
port,
|
||||
};
|
||||
let relay = Self { process, url, port };
|
||||
|
||||
// Wait for relay to be ready
|
||||
relay.wait_for_ready().await;
|
||||
@@ -126,18 +122,18 @@ impl TestRelay {
|
||||
/// Find a free port to use for testing
|
||||
fn find_free_port() -> u16 {
|
||||
use std::net::TcpListener;
|
||||
|
||||
|
||||
// Bind to port 0 to get a random free port
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.expect("Failed to bind to random port");
|
||||
|
||||
let port = listener.local_addr()
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
|
||||
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.expect("Failed to get local address")
|
||||
.port();
|
||||
|
||||
|
||||
// Drop the listener to free the port
|
||||
drop(listener);
|
||||
|
||||
|
||||
port
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user