diff options
| -rw-r--r-- | grasp-audit/src/specs/grasp01_nostr_relay.rs | 147 |
1 files changed, 123 insertions, 24 deletions
diff --git a/grasp-audit/src/specs/grasp01_nostr_relay.rs b/grasp-audit/src/specs/grasp01_nostr_relay.rs index 837434b..19322cb 100644 --- a/grasp-audit/src/specs/grasp01_nostr_relay.rs +++ b/grasp-audit/src/specs/grasp01_nostr_relay.rs | |||
| @@ -212,16 +212,55 @@ impl Grasp01NostrRelayTests { | |||
| 212 | "Reject repository announcements without service in relays tag", | 212 | "Reject repository announcements without service in relays tag", |
| 213 | ) | 213 | ) |
| 214 | .run(|| async { | 214 | .run(|| async { |
| 215 | // TODO: Implementation | 215 | // Get relay URL from client |
| 216 | // 1. Create kind 30617 event with: | 216 | let relay_url = client.client().relays().await |
| 217 | // - d tag: "test-repo-no-relays" | 217 | .keys() |
| 218 | // - clone tag: "{service_url}/{npub}/test-repo.git" (correct) | 218 | .next() |
| 219 | // - relays tag: "wss://relay.damus.io" (NOT this service) | 219 | .ok_or("No relay connected - client has no active relay connections")? |
| 220 | // 2. Send event to relay | 220 | .to_string(); |
| 221 | // 3. Verify rejection | ||
| 222 | // 4. Query to confirm event is NOT in relay | ||
| 223 | 221 | ||
| 224 | Err("Not implemented yet".to_string()) | 222 | // Convert WebSocket URL to HTTP URL for clone tag |
| 223 | let http_url = relay_url | ||
| 224 | .replace("ws://", "http://") | ||
| 225 | .replace("wss://", "https://"); | ||
| 226 | |||
| 227 | // Create unique repository identifier | ||
| 228 | let timestamp = Timestamp::now().as_u64(); | ||
| 229 | let repo_id = format!("test-repo-no-relays-{}", timestamp); | ||
| 230 | |||
| 231 | // Create repo announcement WITHOUT service in relays tag | ||
| 232 | let event = client.event_builder(Kind::GitRepoAnnouncement, "") | ||
| 233 | .tag(Tag::identifier(&repo_id)) | ||
| 234 | .tag(Tag::custom(TagKind::custom("name"), vec!["Test Repo No Relays"])) | ||
| 235 | .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/test-repo.git", http_url, client.public_key())])) // Correct clone | ||
| 236 | .tag(Tag::custom(TagKind::custom("relays"), vec!["wss://relay.damus.io"])) // NOT this service | ||
| 237 | .build(client.keys()) | ||
| 238 | .map_err(|e| format!("Failed to build event: {}", e))?; | ||
| 239 | |||
| 240 | let event_id = event.id; | ||
| 241 | |||
| 242 | // Send event - expect rejection | ||
| 243 | let _send_result = client.send_event(event.clone()).await; | ||
| 244 | |||
| 245 | // Query to verify event is NOT stored | ||
| 246 | let filter = Filter::new() | ||
| 247 | .kind(Kind::GitRepoAnnouncement) | ||
| 248 | .author(client.public_key()) | ||
| 249 | .identifier(&repo_id); | ||
| 250 | |||
| 251 | let events = client.query(filter).await | ||
| 252 | .map_err(|e| format!("Failed to query events from relay: {}", e))?; | ||
| 253 | |||
| 254 | // Verify event was rejected (not stored) | ||
| 255 | if events.iter().any(|e| e.id == event_id) { | ||
| 256 | return Err(format!( | ||
| 257 | "Relay incorrectly accepted announcement without service in relays tag. \ | ||
| 258 | Event ID: {}, Relays URL: wss://relay.damus.io (should require {})", | ||
| 259 | event_id, relay_url | ||
| 260 | )); | ||
| 261 | } | ||
| 262 | |||
| 263 | Ok(()) | ||
| 225 | }) | 264 | }) |
| 226 | .await | 265 | .await |
| 227 | } | 266 | } |
| @@ -232,27 +271,87 @@ impl Grasp01NostrRelayTests { | |||
| 232 | 271 | ||
| 233 | /// Test: Accept valid repository state announcements | 272 | /// Test: Accept valid repository state announcements |
| 234 | /// | 273 | /// |
| 235 | /// Spec: Line 3 of ../grasp/01.md | 274 | /// Spec: Lines 6-7 of ../grasp/01.md |
| 236 | /// Requirement: MUST accept repo state announcements | 275 | /// Requirement: MUST accept repo state announcements with d, maintainers, and r tags |
| 237 | async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult { | 276 | async fn test_accept_valid_repo_state_announcement(client: &AuditClient) -> TestResult { |
| 238 | TestResult::new( | 277 | TestResult::new( |
| 239 | "accept_valid_repo_state_announcement", | 278 | "accept_valid_repo_state_announcement", |
| 240 | "GRASP-01:nostr-relay:3", | 279 | "GRASP-01:nostr-relay:6-7", |
| 241 | "Accept valid repository state announcements", | 280 | "Accept valid repository state announcements with required tags", |
| 242 | ) | 281 | ) |
| 243 | .run(|| async { | 282 | .run(|| async { |
| 244 | // TODO: Implementation | 283 | // Create unique repository identifier |
| 245 | // 1. First send valid kind 30617 (repo announcement) - prerequisite | 284 | let timestamp = Timestamp::now().as_u64(); |
| 246 | // 2. Create kind 30618 event with: | 285 | let repo_id = format!("test-repo-state-{}", timestamp); |
| 247 | // - d tag: same as repo announcement | ||
| 248 | // - refs/heads/main tag: "{commit-sha}" | ||
| 249 | // - HEAD tag: "ref: refs/heads/main" | ||
| 250 | // 3. Send state announcement | ||
| 251 | // 4. Verify acceptance | ||
| 252 | // 5. Query back to confirm stored | ||
| 253 | // 6. Verify all tags are preserved | ||
| 254 | 286 | ||
| 255 | Err("Not implemented yet".to_string()) | 287 | // Create kind 30618 repository state announcement with required tags |
| 288 | let npub = client.public_key().to_bech32() | ||
| 289 | .map_err(|e| format!("Failed to convert public key to bech32: {}", e))?; | ||
| 290 | |||
| 291 | let event = client.event_builder(Kind::Custom(30618), "") | ||
| 292 | .tag(Tag::identifier(&repo_id)) | ||
| 293 | .tag(Tag::custom(TagKind::custom("maintainers"), vec![npub])) | ||
| 294 | .tag(Tag::custom(TagKind::custom("r"), vec!["refs/heads/main".to_string()])) | ||
| 295 | .build(client.keys()) | ||
| 296 | .map_err(|e| format!("Failed to build repository state announcement: {}", e))?; | ||
| 297 | |||
| 298 | let event_id = event.id; | ||
| 299 | |||
| 300 | // Send the event | ||
| 301 | client.send_event(event.clone()).await | ||
| 302 | .map_err(|e| format!("Failed to send repository state announcement to relay: {}", e))?; | ||
| 303 | |||
| 304 | // Query back to verify it was accepted and stored | ||
| 305 | let filter = Filter::new() | ||
| 306 | .kind(Kind::Custom(30618)) | ||
| 307 | .author(client.public_key()) | ||
| 308 | .identifier(&repo_id); | ||
| 309 | |||
| 310 | let events = client.query(filter).await | ||
| 311 | .map_err(|e| format!("Failed to query events from relay: {}", e))?; | ||
| 312 | |||
| 313 | // Verify we got the event back | ||
| 314 | if events.is_empty() { | ||
| 315 | return Err(format!( | ||
| 316 | "Event was not stored in relay (possibly rejected). Event ID: {}, Repo ID: {}", | ||
| 317 | event_id, repo_id | ||
| 318 | )); | ||
| 319 | } | ||
| 320 | |||
| 321 | // Verify it's the same event | ||
| 322 | let stored_event = events.iter() | ||
| 323 | .find(|e| e.id == event_id) | ||
| 324 | .ok_or(format!( | ||
| 325 | "Stored event ID doesn't match sent event. Expected: {}, Got {} events", | ||
| 326 | event_id, events.len() | ||
| 327 | ))?; | ||
| 328 | |||
| 329 | // Verify required tags are present | ||
| 330 | let has_d_tag = stored_event.tags.iter() | ||
| 331 | .any(|t| t.kind() == TagKind::d() && t.content() == Some(&repo_id)); | ||
| 332 | |||
| 333 | let has_maintainers_tag = stored_event.tags.iter() | ||
| 334 | .any(|t| t.kind() == TagKind::custom("maintainers")); | ||
| 335 | |||
| 336 | let has_r_tag = stored_event.tags.iter() | ||
| 337 | .any(|t| { | ||
| 338 | t.kind() == TagKind::custom("r") && | ||
| 339 | t.content().map(|c| c.contains("refs/heads/main")).unwrap_or(false) | ||
| 340 | }); | ||
| 341 | |||
| 342 | if !has_d_tag { | ||
| 343 | return Err(format!("Stored event missing d tag with repo identifier ({})", repo_id)); | ||
| 344 | } | ||
| 345 | |||
| 346 | if !has_maintainers_tag { | ||
| 347 | return Err("Stored event missing maintainers tag".to_string()); | ||
| 348 | } | ||
| 349 | |||
| 350 | if !has_r_tag { | ||
| 351 | return Err("Stored event missing r tag with git reference".to_string()); | ||
| 352 | } | ||
| 353 | |||
| 354 | Ok(()) | ||
| 256 | }) | 355 | }) |
| 257 | .await | 356 | .await |
| 258 | } | 357 | } |