diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-10 16:13:51 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-10 16:13:51 +0000 |
| commit | d08878d0b9a8738e57e457a916677d2061775cbd (patch) | |
| tree | 7bbb4a185304615e4cc587cd23052de7a7cfea3a /tests/common | |
| parent | 8148c27a1350189046bc8e215f29f918dd8747f5 (diff) | |
Phase 5: Migrate bootstrap and discovery tests
Create organized test structure for proactive sync:
tests/common/sync_helpers.rs (from Phase 4):
- TestClient with retry logic for connect/send
- Event builders: build_layer2_issue_event, build_layer3_comment_event
- Tag variants (a/A/q for Layer 2, e/E/q for Layer 3)
- wait_for_event_on_relay() assertion helper
- repo_coord() utility function
- Unit tests for all builders
tests/sync/mod.rs:
- Module organization for sync tests
- Documentation of test categories
tests/sync.rs:
- Main test harness including common and sync modules
tests/sync/bootstrap.rs:
- test_bootstrap_syncs_existing_layer2_events (Test 1)
- test_relay_replays_events_after_restart (Test 4)
tests/sync/discovery.rs:
- test_discovers_layer3_via_layer2 (Test 2)
- test_layer2_discovery_with_chain (Test 3 - simplified)
All 14 tests pass: cargo test --test sync
Diffstat (limited to 'tests/common')
| -rw-r--r-- | tests/common/mod.rs | 2 | ||||
| -rw-r--r-- | tests/common/sync_helpers.rs | 563 |
2 files changed, 565 insertions, 0 deletions
diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 76ed273..9bbfb40 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs | |||
| @@ -1,5 +1,7 @@ | |||
| 1 | //! Common test utilities | 1 | //! Common test utilities |
| 2 | 2 | ||
| 3 | pub mod relay; | 3 | pub mod relay; |
| 4 | pub mod sync_helpers; | ||
| 4 | 5 | ||
| 5 | pub use relay::TestRelay; | 6 | pub use relay::TestRelay; |
| 7 | pub use sync_helpers::*; | ||
diff --git a/tests/common/sync_helpers.rs b/tests/common/sync_helpers.rs new file mode 100644 index 0000000..d9be332 --- /dev/null +++ b/tests/common/sync_helpers.rs | |||
| @@ -0,0 +1,563 @@ | |||
| 1 | //! Proactive Sync Test Helpers | ||
| 2 | //! | ||
| 3 | //! Provides utilities for testing ngit-grasp's proactive sync functionality: | ||
| 4 | //! - `TestClient` - Client wrapper with built-in retry logic | ||
| 5 | //! - Event builders for Layer 2 (kind 1618) and Layer 3 (kinds 1, 1111) events | ||
| 6 | //! - Assertion helpers that return bool (non-panicking) | ||
| 7 | //! | ||
| 8 | //! # nostr-sdk 0.43 API Notes | ||
| 9 | //! - Use field access: `event.id`, `event.tags`, `event.tags.iter()` | ||
| 10 | //! - Use `Tag::custom(TagKind::custom("name"), vec![...])` syntax | ||
| 11 | //! - Use `EventBuilder::new(kind, content).tags(tags)` syntax | ||
| 12 | |||
| 13 | use std::time::Duration; | ||
| 14 | |||
| 15 | use nostr_sdk::prelude::*; | ||
| 16 | |||
| 17 | /// Kind 1618 - Issue (NIP-34 git-related event) | ||
| 18 | pub const KIND_ISSUE: u16 = 1618; | ||
| 19 | |||
| 20 | /// Kind 1111 - NIP-22 Comment | ||
| 21 | pub const KIND_COMMENT: u16 = 1111; | ||
| 22 | |||
| 23 | /// Kind 30617 - Repository state/announcement (NIP-34) | ||
| 24 | pub const KIND_REPOSITORY_STATE: u16 = 30617; | ||
| 25 | |||
| 26 | /// Test client with built-in retry logic for connect and send operations. | ||
| 27 | /// | ||
| 28 | /// Wraps nostr-sdk Client with automatic retry handling suitable for | ||
| 29 | /// integration tests where connections may take time to establish. | ||
| 30 | pub struct TestClient { | ||
| 31 | client: Client, | ||
| 32 | relay_url: String, | ||
| 33 | keys: Keys, | ||
| 34 | } | ||
| 35 | |||
| 36 | impl TestClient { | ||
| 37 | /// Create a new TestClient and connect to the specified relay. | ||
| 38 | /// | ||
| 39 | /// Uses retry logic: up to 30 attempts with 100ms delay between each. | ||
| 40 | /// | ||
| 41 | /// # Arguments | ||
| 42 | /// * `relay_url` - WebSocket URL of the relay (e.g., "ws://127.0.0.1:8080") | ||
| 43 | /// * `keys` - Nostr keys for signing events | ||
| 44 | /// | ||
| 45 | /// # Returns | ||
| 46 | /// * `Ok(TestClient)` on successful connection | ||
| 47 | /// * `Err(String)` if connection fails after all retries | ||
| 48 | pub async fn new(relay_url: &str, keys: Keys) -> Result<Self, String> { | ||
| 49 | let client = Client::new(keys.clone()); | ||
| 50 | |||
| 51 | client | ||
| 52 | .add_relay(relay_url) | ||
| 53 | .await | ||
| 54 | .map_err(|e| format!("Failed to add relay: {}", e))?; | ||
| 55 | |||
| 56 | let test_client = Self { | ||
| 57 | client, | ||
| 58 | relay_url: relay_url.to_string(), | ||
| 59 | keys, | ||
| 60 | }; | ||
| 61 | |||
| 62 | test_client.connect().await?; | ||
| 63 | |||
| 64 | Ok(test_client) | ||
| 65 | } | ||
| 66 | |||
| 67 | /// Connect to the relay with retry logic. | ||
| 68 | /// | ||
| 69 | /// Attempts connection up to 30 times with 100ms delays (3 seconds total). | ||
| 70 | pub async fn connect(&self) -> Result<(), String> { | ||
| 71 | self.client.connect().await; | ||
| 72 | |||
| 73 | // Wait for connection with retries (matching existing pattern) | ||
| 74 | for attempt in 0..30 { | ||
| 75 | tokio::time::sleep(Duration::from_millis(100)).await; | ||
| 76 | let relays = self.client.relays().await; | ||
| 77 | if relays.values().any(|r| r.is_connected()) { | ||
| 78 | return Ok(()); | ||
| 79 | } | ||
| 80 | if attempt == 29 { | ||
| 81 | return Err(format!( | ||
| 82 | "Failed to connect to relay {} after 3 seconds", | ||
| 83 | self.relay_url | ||
| 84 | )); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | Err("Connection loop exited unexpectedly".to_string()) | ||
| 89 | } | ||
| 90 | |||
| 91 | /// Send an event with retry logic. | ||
| 92 | /// | ||
| 93 | /// Attempts to send up to 3 times with exponential backoff: | ||
| 94 | /// - Attempt 1: immediate | ||
| 95 | /// - Attempt 2: after 200ms | ||
| 96 | /// - Attempt 3: after 400ms | ||
| 97 | /// | ||
| 98 | /// # Arguments | ||
| 99 | /// * `event` - The signed event to send | ||
| 100 | /// | ||
| 101 | /// # Returns | ||
| 102 | /// * `Ok(EventId)` on successful send | ||
| 103 | /// * `Err(String)` if all attempts fail | ||
| 104 | pub async fn send_event(&self, event: &Event) -> Result<EventId, String> { | ||
| 105 | let delays = [0, 200, 400]; // Exponential backoff in ms | ||
| 106 | |||
| 107 | for (attempt, delay_ms) in delays.iter().enumerate() { | ||
| 108 | if *delay_ms > 0 { | ||
| 109 | tokio::time::sleep(Duration::from_millis(*delay_ms)).await; | ||
| 110 | } | ||
| 111 | |||
| 112 | match self.client.send_event(event).await { | ||
| 113 | Ok(output) => { | ||
| 114 | if !output.success.is_empty() { | ||
| 115 | return Ok(output.val); | ||
| 116 | } | ||
| 117 | // Log failures for debugging | ||
| 118 | if !output.failed.is_empty() { | ||
| 119 | eprintln!( | ||
| 120 | " Send attempt {} - failures: {:?}", | ||
| 121 | attempt + 1, | ||
| 122 | output.failed | ||
| 123 | ); | ||
| 124 | // Try reconnecting if relay disconnected | ||
| 125 | self.client.connect().await; | ||
| 126 | } | ||
| 127 | } | ||
| 128 | Err(e) => { | ||
| 129 | eprintln!(" Send attempt {} - error: {}", attempt + 1, e); | ||
| 130 | } | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | Err(format!( | ||
| 135 | "Failed to send event {} after 3 attempts", | ||
| 136 | event.id | ||
| 137 | )) | ||
| 138 | } | ||
| 139 | |||
| 140 | /// Get a reference to the keys used by this client. | ||
| 141 | pub fn keys(&self) -> &Keys { | ||
| 142 | &self.keys | ||
| 143 | } | ||
| 144 | |||
| 145 | /// Disconnect from the relay. | ||
| 146 | pub async fn disconnect(self) { | ||
| 147 | self.client.disconnect().await; | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | // ============================================================================ | ||
| 152 | // Event Builders | ||
| 153 | // ============================================================================ | ||
| 154 | |||
| 155 | /// Build a Layer 2 issue event (kind 1618) with a/A/q tags referencing a repository. | ||
| 156 | /// | ||
| 157 | /// Creates an issue event that references the specified repository coordinate. | ||
| 158 | /// Supports different tag types for comprehensive Layer 2 filter testing. | ||
| 159 | /// | ||
| 160 | /// # Arguments | ||
| 161 | /// * `keys` - Keys for signing the event | ||
| 162 | /// * `repo_coord` - Repository coordinate (format: "30617:pubkey_hex:identifier") | ||
| 163 | /// * `title` - Issue title (used as content) | ||
| 164 | /// | ||
| 165 | /// # Tag Types | ||
| 166 | /// Uses lowercase 'a' tag by default. For other tag variations, see: | ||
| 167 | /// - `build_layer2_issue_with_uppercase_a_tag` | ||
| 168 | /// - `build_layer2_issue_with_q_tag` | ||
| 169 | /// | ||
| 170 | /// # Returns | ||
| 171 | /// * `Ok(Event)` - Signed event ready to send | ||
| 172 | /// * `Err(String)` - If signing fails | ||
| 173 | pub fn build_layer2_issue_event(keys: &Keys, repo_coord: &str, title: &str) -> Result<Event, String> { | ||
| 174 | build_layer2_issue_with_tag(keys, repo_coord, title, TagVariant::LowercaseA) | ||
| 175 | } | ||
| 176 | |||
| 177 | /// Build a Layer 2 issue with uppercase 'A' tag. | ||
| 178 | pub fn build_layer2_issue_with_uppercase_a_tag( | ||
| 179 | keys: &Keys, | ||
| 180 | repo_coord: &str, | ||
| 181 | title: &str, | ||
| 182 | ) -> Result<Event, String> { | ||
| 183 | build_layer2_issue_with_tag(keys, repo_coord, title, TagVariant::UppercaseA) | ||
| 184 | } | ||
| 185 | |||
| 186 | /// Build a Layer 2 issue with 'q' (quote) tag. | ||
| 187 | pub fn build_layer2_issue_with_q_tag( | ||
| 188 | keys: &Keys, | ||
| 189 | repo_coord: &str, | ||
| 190 | title: &str, | ||
| 191 | ) -> Result<Event, String> { | ||
| 192 | build_layer2_issue_with_tag(keys, repo_coord, title, TagVariant::QuoteQ) | ||
| 193 | } | ||
| 194 | |||
| 195 | /// Tag variant for Layer 2 events (referencing repo coordinates) | ||
| 196 | #[derive(Debug, Clone, Copy)] | ||
| 197 | pub enum TagVariant { | ||
| 198 | /// Lowercase 'a' tag - standard addressable reference | ||
| 199 | LowercaseA, | ||
| 200 | /// Uppercase 'A' tag - some clients use this | ||
| 201 | UppercaseA, | ||
| 202 | /// Quote 'q' tag - NIP-10 quote reference | ||
| 203 | QuoteQ, | ||
| 204 | } | ||
| 205 | |||
| 206 | /// Internal helper to build Layer 2 issue with specified tag variant. | ||
| 207 | fn build_layer2_issue_with_tag( | ||
| 208 | keys: &Keys, | ||
| 209 | repo_coord: &str, | ||
| 210 | title: &str, | ||
| 211 | tag_variant: TagVariant, | ||
| 212 | ) -> Result<Event, String> { | ||
| 213 | let tag = match tag_variant { | ||
| 214 | TagVariant::LowercaseA => Tag::custom(TagKind::custom("a"), vec![repo_coord.to_string()]), | ||
| 215 | TagVariant::UppercaseA => Tag::custom(TagKind::custom("A"), vec![repo_coord.to_string()]), | ||
| 216 | TagVariant::QuoteQ => Tag::custom(TagKind::custom("q"), vec![repo_coord.to_string()]), | ||
| 217 | }; | ||
| 218 | |||
| 219 | let tags = vec![tag]; | ||
| 220 | |||
| 221 | EventBuilder::new(Kind::Custom(KIND_ISSUE), title) | ||
| 222 | .tags(tags) | ||
| 223 | .sign_with_keys(keys) | ||
| 224 | .map_err(|e| format!("Failed to sign Layer 2 issue event: {}", e)) | ||
| 225 | } | ||
| 226 | |||
| 227 | /// Build a Layer 3 comment event (kinds 1 or 1111) with e/E/q tags referencing an event ID. | ||
| 228 | /// | ||
| 229 | /// Creates a comment/reply event that references the specified parent event ID. | ||
| 230 | /// Supports different kinds and tag types for comprehensive Layer 3 filter testing. | ||
| 231 | /// | ||
| 232 | /// # Arguments | ||
| 233 | /// * `keys` - Keys for signing the event | ||
| 234 | /// * `parent_event_id` - Event ID being referenced (e.g., an issue or patch) | ||
| 235 | /// * `content` - Comment content | ||
| 236 | /// * `kind` - Event kind (Kind::Custom(1) for reply, Kind::Custom(1111) for NIP-22 comment) | ||
| 237 | /// | ||
| 238 | /// # Tag Types | ||
| 239 | /// - For kind 1111: Uses uppercase 'E' tag (NIP-22 style) | ||
| 240 | /// - For kind 1: Uses lowercase 'e' tag with "root" marker (NIP-10 style) | ||
| 241 | /// | ||
| 242 | /// # Returns | ||
| 243 | /// * `Ok(Event)` - Signed event ready to send | ||
| 244 | /// * `Err(String)` - If signing fails | ||
| 245 | pub fn build_layer3_comment_event( | ||
| 246 | keys: &Keys, | ||
| 247 | parent_event_id: &EventId, | ||
| 248 | content: &str, | ||
| 249 | kind: Kind, | ||
| 250 | ) -> Result<Event, String> { | ||
| 251 | let kind_num = kind.as_u16(); | ||
| 252 | |||
| 253 | // Choose tag based on kind (NIP-22 uses E, NIP-10 style uses e) | ||
| 254 | let tag = if kind_num == KIND_COMMENT { | ||
| 255 | // NIP-22 comment: uppercase 'E' tag | ||
| 256 | Tag::custom( | ||
| 257 | TagKind::custom("E"), | ||
| 258 | vec![parent_event_id.to_hex()], | ||
| 259 | ) | ||
| 260 | } else { | ||
| 261 | // Kind 1 reply: lowercase 'e' tag with root marker (NIP-10) | ||
| 262 | Tag::custom( | ||
| 263 | TagKind::custom("e"), | ||
| 264 | vec![parent_event_id.to_hex(), "".to_string(), "root".to_string()], | ||
| 265 | ) | ||
| 266 | }; | ||
| 267 | |||
| 268 | let tags = vec![tag]; | ||
| 269 | |||
| 270 | EventBuilder::new(kind, content) | ||
| 271 | .tags(tags) | ||
| 272 | .sign_with_keys(keys) | ||
| 273 | .map_err(|e| format!("Failed to sign Layer 3 comment event: {}", e)) | ||
| 274 | } | ||
| 275 | |||
| 276 | /// Build a Layer 3 reply (kind 1) with lowercase 'e' tag. | ||
| 277 | pub fn build_layer3_reply_with_e_tag( | ||
| 278 | keys: &Keys, | ||
| 279 | parent_event_id: &EventId, | ||
| 280 | content: &str, | ||
| 281 | ) -> Result<Event, String> { | ||
| 282 | let tag = Tag::custom( | ||
| 283 | TagKind::custom("e"), | ||
| 284 | vec![parent_event_id.to_hex(), "".to_string(), "root".to_string()], | ||
| 285 | ); | ||
| 286 | |||
| 287 | EventBuilder::new(Kind::Custom(1), content) | ||
| 288 | .tags(vec![tag]) | ||
| 289 | .sign_with_keys(keys) | ||
| 290 | .map_err(|e| format!("Failed to sign Layer 3 reply event: {}", e)) | ||
| 291 | } | ||
| 292 | |||
| 293 | /// Build a Layer 3 comment (kind 1111) with uppercase 'E' tag (NIP-22). | ||
| 294 | pub fn build_layer3_comment_with_uppercase_e_tag( | ||
| 295 | keys: &Keys, | ||
| 296 | parent_event_id: &EventId, | ||
| 297 | content: &str, | ||
| 298 | ) -> Result<Event, String> { | ||
| 299 | let tag = Tag::custom( | ||
| 300 | TagKind::custom("E"), | ||
| 301 | vec![parent_event_id.to_hex()], | ||
| 302 | ); | ||
| 303 | |||
| 304 | EventBuilder::new(Kind::Custom(KIND_COMMENT), content) | ||
| 305 | .tags(vec![tag]) | ||
| 306 | .sign_with_keys(keys) | ||
| 307 | .map_err(|e| format!("Failed to sign Layer 3 comment event: {}", e)) | ||
| 308 | } | ||
| 309 | |||
| 310 | /// Build a Layer 3 quote (kind 1) with 'q' tag. | ||
| 311 | pub fn build_layer3_quote_with_q_tag( | ||
| 312 | keys: &Keys, | ||
| 313 | parent_event_id: &EventId, | ||
| 314 | content: &str, | ||
| 315 | ) -> Result<Event, String> { | ||
| 316 | let tag = Tag::custom( | ||
| 317 | TagKind::custom("q"), | ||
| 318 | vec![parent_event_id.to_hex()], | ||
| 319 | ); | ||
| 320 | |||
| 321 | EventBuilder::new(Kind::Custom(1), content) | ||
| 322 | .tags(vec![tag]) | ||
| 323 | .sign_with_keys(keys) | ||
| 324 | .map_err(|e| format!("Failed to sign Layer 3 quote event: {}", e)) | ||
| 325 | } | ||
| 326 | |||
| 327 | // ============================================================================ | ||
| 328 | // Assertion Helpers | ||
| 329 | // ============================================================================ | ||
| 330 | |||
| 331 | /// Wait for an event to appear on a relay. | ||
| 332 | /// | ||
| 333 | /// Polls the relay for the specified event using the provided filter. | ||
| 334 | /// Returns true if found within timeout, false otherwise. | ||
| 335 | /// | ||
| 336 | /// **Important:** This function does NOT panic - it returns a bool to allow | ||
| 337 | /// tests to make their own assertions with descriptive error messages. | ||
| 338 | /// | ||
| 339 | /// # Arguments | ||
| 340 | /// * `relay_url` - WebSocket URL of the relay to check | ||
| 341 | /// * `filter` - Nostr filter to use for querying (should match the expected event) | ||
| 342 | /// * `timeout` - Maximum time to wait for the event | ||
| 343 | /// | ||
| 344 | /// # Returns | ||
| 345 | /// * `true` - Event matching filter was found | ||
| 346 | /// * `false` - Event not found within timeout, or connection failed | ||
| 347 | /// | ||
| 348 | /// # Example | ||
| 349 | /// ```ignore | ||
| 350 | /// let filter = Filter::new() | ||
| 351 | /// .kind(Kind::Custom(1618)) | ||
| 352 | /// .author(keys.public_key()) | ||
| 353 | /// .id(event.id); | ||
| 354 | /// | ||
| 355 | /// let found = wait_for_event_on_relay(relay.url(), filter, Duration::from_secs(3)).await; | ||
| 356 | /// assert!(found, "Expected event {} to sync to relay", event.id); | ||
| 357 | /// ``` | ||
| 358 | pub async fn wait_for_event_on_relay(relay_url: &str, filter: Filter, timeout: Duration) -> bool { | ||
| 359 | // Create a temporary client for querying | ||
| 360 | let temp_keys = Keys::generate(); | ||
| 361 | let client = Client::new(temp_keys); | ||
| 362 | |||
| 363 | // Try to connect | ||
| 364 | if client.add_relay(relay_url).await.is_err() { | ||
| 365 | return false; | ||
| 366 | } | ||
| 367 | |||
| 368 | client.connect().await; | ||
| 369 | |||
| 370 | // Wait for connection (brief timeout) | ||
| 371 | let mut connected = false; | ||
| 372 | for _ in 0..10 { | ||
| 373 | tokio::time::sleep(Duration::from_millis(100)).await; | ||
| 374 | let relays = client.relays().await; | ||
| 375 | if relays.values().any(|r| r.is_connected()) { | ||
| 376 | connected = true; | ||
| 377 | break; | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | if !connected { | ||
| 382 | client.disconnect().await; | ||
| 383 | return false; | ||
| 384 | } | ||
| 385 | |||
| 386 | // Fetch events with the provided timeout | ||
| 387 | let result = client.fetch_events(filter, timeout).await; | ||
| 388 | |||
| 389 | client.disconnect().await; | ||
| 390 | |||
| 391 | match result { | ||
| 392 | Ok(events) => !events.is_empty(), | ||
| 393 | Err(_) => false, | ||
| 394 | } | ||
| 395 | } | ||
| 396 | |||
| 397 | /// Build repo coordinate string for use in 'a' tags. | ||
| 398 | /// | ||
| 399 | /// Format: `30617:pubkey_hex:identifier` | ||
| 400 | /// | ||
| 401 | /// # Arguments | ||
| 402 | /// * `keys` - Keys whose public key will be used | ||
| 403 | /// * `identifier` - Repository identifier (d-tag value) | ||
| 404 | pub fn repo_coord(keys: &Keys, identifier: &str) -> String { | ||
| 405 | format!( | ||
| 406 | "{}:{}:{}", | ||
| 407 | KIND_REPOSITORY_STATE, | ||
| 408 | keys.public_key().to_hex(), | ||
| 409 | identifier | ||
| 410 | ) | ||
| 411 | } | ||
| 412 | |||
| 413 | #[cfg(test)] | ||
| 414 | mod tests { | ||
| 415 | use super::*; | ||
| 416 | |||
| 417 | #[test] | ||
| 418 | fn test_repo_coord_format() { | ||
| 419 | let keys = Keys::generate(); | ||
| 420 | let coord = repo_coord(&keys, "test-repo"); | ||
| 421 | |||
| 422 | assert!(coord.starts_with("30617:")); | ||
| 423 | assert!(coord.ends_with(":test-repo")); | ||
| 424 | assert_eq!(coord.split(':').count(), 3); | ||
| 425 | } | ||
| 426 | |||
| 427 | #[test] | ||
| 428 | fn test_build_layer2_issue_event() { | ||
| 429 | let keys = Keys::generate(); | ||
| 430 | let coord = repo_coord(&keys, "my-repo"); | ||
| 431 | |||
| 432 | let event = build_layer2_issue_event(&keys, &coord, "Test Issue") | ||
| 433 | .expect("Should create event"); | ||
| 434 | |||
| 435 | // nostr-sdk 0.43: use field access | ||
| 436 | assert_eq!(event.kind.as_u16(), KIND_ISSUE); | ||
| 437 | |||
| 438 | // Check the tag exists | ||
| 439 | let has_a_tag = event.tags.iter().any(|tag| { | ||
| 440 | let slice = tag.as_slice(); | ||
| 441 | slice.first().is_some_and(|t| t == "a") | ||
| 442 | }); | ||
| 443 | assert!(has_a_tag, "Event should have 'a' tag"); | ||
| 444 | } | ||
| 445 | |||
| 446 | #[test] | ||
| 447 | fn test_build_layer2_issue_with_uppercase_a() { | ||
| 448 | let keys = Keys::generate(); | ||
| 449 | let coord = repo_coord(&keys, "my-repo"); | ||
| 450 | |||
| 451 | let event = build_layer2_issue_with_uppercase_a_tag(&keys, &coord, "Test Issue") | ||
| 452 | .expect("Should create event"); | ||
| 453 | |||
| 454 | let has_upper_a_tag = event.tags.iter().any(|tag| { | ||
| 455 | let slice = tag.as_slice(); | ||
| 456 | slice.first().is_some_and(|t| t == "A") | ||
| 457 | }); | ||
| 458 | assert!(has_upper_a_tag, "Event should have 'A' tag"); | ||
| 459 | } | ||
| 460 | |||
| 461 | #[test] | ||
| 462 | fn test_build_layer2_issue_with_q_tag() { | ||
| 463 | let keys = Keys::generate(); | ||
| 464 | let coord = repo_coord(&keys, "my-repo"); | ||
| 465 | |||
| 466 | let event = build_layer2_issue_with_q_tag(&keys, &coord, "Test Issue") | ||
| 467 | .expect("Should create event"); | ||
| 468 | |||
| 469 | let has_q_tag = event.tags.iter().any(|tag| { | ||
| 470 | let slice = tag.as_slice(); | ||
| 471 | slice.first().is_some_and(|t| t == "q") | ||
| 472 | }); | ||
| 473 | assert!(has_q_tag, "Event should have 'q' tag"); | ||
| 474 | } | ||
| 475 | |||
| 476 | #[test] | ||
| 477 | fn test_build_layer3_comment_kind_1111() { | ||
| 478 | let keys = Keys::generate(); | ||
| 479 | let parent_id = EventId::all_zeros(); | ||
| 480 | |||
| 481 | let event = build_layer3_comment_event(&keys, &parent_id, "Test comment", Kind::Custom(KIND_COMMENT)) | ||
| 482 | .expect("Should create event"); | ||
| 483 | |||
| 484 | assert_eq!(event.kind.as_u16(), KIND_COMMENT); | ||
| 485 | |||
| 486 | // NIP-22 comment should have uppercase 'E' tag | ||
| 487 | let has_e_tag = event.tags.iter().any(|tag| { | ||
| 488 | let slice = tag.as_slice(); | ||
| 489 | slice.first().is_some_and(|t| t == "E") | ||
| 490 | }); | ||
| 491 | assert!(has_e_tag, "Kind 1111 event should have 'E' tag"); | ||
| 492 | } | ||
| 493 | |||
| 494 | #[test] | ||
| 495 | fn test_build_layer3_comment_kind_1() { | ||
| 496 | let keys = Keys::generate(); | ||
| 497 | let parent_id = EventId::all_zeros(); | ||
| 498 | |||
| 499 | let event = build_layer3_comment_event(&keys, &parent_id, "Test reply", Kind::Custom(1)) | ||
| 500 | .expect("Should create event"); | ||
| 501 | |||
| 502 | assert_eq!(event.kind.as_u16(), 1); | ||
| 503 | |||
| 504 | // Kind 1 reply should have lowercase 'e' tag with root marker | ||
| 505 | let has_e_tag = event.tags.iter().any(|tag| { | ||
| 506 | let slice = tag.as_slice(); | ||
| 507 | slice.first().is_some_and(|t| t == "e") | ||
| 508 | }); | ||
| 509 | assert!(has_e_tag, "Kind 1 event should have 'e' tag"); | ||
| 510 | } | ||
| 511 | |||
| 512 | #[test] | ||
| 513 | fn test_build_layer3_reply_with_e_tag() { | ||
| 514 | let keys = Keys::generate(); | ||
| 515 | let parent_id = EventId::all_zeros(); | ||
| 516 | |||
| 517 | let event = build_layer3_reply_with_e_tag(&keys, &parent_id, "Reply content") | ||
| 518 | .expect("Should create event"); | ||
| 519 | |||
| 520 | assert_eq!(event.kind.as_u16(), 1); | ||
| 521 | |||
| 522 | let has_e_tag = event.tags.iter().any(|tag| { | ||
| 523 | let slice = tag.as_slice(); | ||
| 524 | slice.first().is_some_and(|t| t == "e") && | ||
| 525 | slice.get(3).is_some_and(|m| m == "root") | ||
| 526 | }); | ||
| 527 | assert!(has_e_tag, "Should have 'e' tag with root marker"); | ||
| 528 | } | ||
| 529 | |||
| 530 | #[test] | ||
| 531 | fn test_build_layer3_comment_with_uppercase_e() { | ||
| 532 | let keys = Keys::generate(); | ||
| 533 | let parent_id = EventId::all_zeros(); | ||
| 534 | |||
| 535 | let event = build_layer3_comment_with_uppercase_e_tag(&keys, &parent_id, "Comment content") | ||
| 536 | .expect("Should create event"); | ||
| 537 | |||
| 538 | assert_eq!(event.kind.as_u16(), KIND_COMMENT); | ||
| 539 | |||
| 540 | let has_upper_e_tag = event.tags.iter().any(|tag| { | ||
| 541 | let slice = tag.as_slice(); | ||
| 542 | slice.first().is_some_and(|t| t == "E") | ||
| 543 | }); | ||
| 544 | assert!(has_upper_e_tag, "Should have uppercase 'E' tag"); | ||
| 545 | } | ||
| 546 | |||
| 547 | #[test] | ||
| 548 | fn test_build_layer3_quote_with_q() { | ||
| 549 | let keys = Keys::generate(); | ||
| 550 | let parent_id = EventId::all_zeros(); | ||
| 551 | |||
| 552 | let event = build_layer3_quote_with_q_tag(&keys, &parent_id, "Quote content") | ||
| 553 | .expect("Should create event"); | ||
| 554 | |||
| 555 | assert_eq!(event.kind.as_u16(), 1); | ||
| 556 | |||
| 557 | let has_q_tag = event.tags.iter().any(|tag| { | ||
| 558 | let slice = tag.as_slice(); | ||
| 559 | slice.first().is_some_and(|t| t == "q") | ||
| 560 | }); | ||
| 561 | assert!(has_q_tag, "Should have 'q' tag"); | ||
| 562 | } | ||
| 563 | } \ No newline at end of file | ||