upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests/common
diff options
context:
space:
mode:
Diffstat (limited to 'tests/common')
-rw-r--r--tests/common/mod.rs5
-rw-r--r--tests/common/relay.rs179
2 files changed, 184 insertions, 0 deletions
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
new file mode 100644
index 0000000..76ed273
--- /dev/null
+++ b/tests/common/mod.rs
@@ -0,0 +1,5 @@
1//! Common test utilities
2
3pub mod relay;
4
5pub use relay::TestRelay;
diff --git a/tests/common/relay.rs b/tests/common/relay.rs
new file mode 100644
index 0000000..4208278
--- /dev/null
+++ b/tests/common/relay.rs
@@ -0,0 +1,179 @@
1//! Test relay fixture
2//!
3//! Provides automatic relay lifecycle management for integration tests.
4
5use std::process::{Child, Command, Stdio};
6use std::time::Duration;
7use tokio::time::sleep;
8
9/// Test relay fixture that manages relay lifecycle
10///
11/// Automatically starts and stops the ngit-grasp relay for testing.
12/// Uses a random port to avoid conflicts.
13pub struct TestRelay {
14 process: Child,
15 url: String,
16 port: u16,
17}
18
19impl TestRelay {
20 /// Start a test relay instance
21 ///
22 /// # Example
23 ///
24 /// ```no_run
25 /// use common::TestRelay;
26 ///
27 /// #[tokio::test]
28 /// async fn test_something() {
29 /// let relay = TestRelay::start().await;
30 /// // Use relay.url() for testing
31 /// relay.stop().await;
32 /// }
33 /// ```
34 pub async fn start() -> Self {
35 Self::start_with_port(Self::find_free_port()).await
36 }
37
38 /// Start relay on a specific port
39 pub async fn start_with_port(port: u16) -> Self {
40 let bind_address = format!("127.0.0.1:{}", port);
41 let url = format!("ws://127.0.0.1:{}", port);
42
43 // Use the built binary directly (faster than cargo run)
44 let binary_path = std::env::current_exe()
45 .expect("Failed to get current exe")
46 .parent()
47 .expect("Failed to get parent dir")
48 .parent()
49 .expect("Failed to get grandparent dir")
50 .join("ngit-grasp");
51
52 // Start the relay process
53 let process = Command::new(&binary_path)
54 .env("NGIT_BIND_ADDRESS", &bind_address)
55 .env("NGIT_DOMAIN", &bind_address) // Set domain to match bind address
56 .env("RUST_LOG", "warn") // Less logging during tests
57 .stdout(Stdio::null())
58 .stderr(Stdio::null())
59 .spawn()
60 .expect("Failed to start relay process");
61
62 let relay = Self {
63 process,
64 url,
65 port,
66 };
67
68 // Wait for relay to be ready
69 relay.wait_for_ready().await;
70
71 relay
72 }
73
74 /// Get the relay WebSocket URL
75 pub fn url(&self) -> &str {
76 &self.url
77 }
78
79 /// Get the relay port
80 pub fn port(&self) -> u16 {
81 self.port
82 }
83
84 /// Get the relay domain (host:port)
85 pub fn domain(&self) -> String {
86 format!("127.0.0.1:{}", self.port)
87 }
88
89 /// Wait for the relay to be ready to accept connections
90 async fn wait_for_ready(&self) {
91 let max_attempts = 50; // 5 seconds total
92 let delay = Duration::from_millis(100);
93
94 for attempt in 0..max_attempts {
95 // Try to connect to the relay
96 match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", self.port)).await {
97 Ok(_) => {
98 // Connection successful, relay is ready
99 // Give it a tiny bit more time to fully initialize
100 sleep(Duration::from_millis(100)).await;
101 return;
102 }
103 Err(_) => {
104 if attempt == max_attempts - 1 {
105 panic!("Relay failed to start after {} attempts", max_attempts);
106 }
107 sleep(delay).await;
108 }
109 }
110 }
111 }
112
113 /// Stop the relay
114 pub async fn stop(mut self) {
115 // Send SIGTERM to gracefully shutdown
116 #[cfg(unix)]
117 {
118 use nix::sys::signal::{kill, Signal};
119 use nix::unistd::Pid;
120
121 let pid = Pid::from_raw(self.process.id() as i32);
122 let _ = kill(pid, Signal::SIGTERM);
123 }
124
125 // Wait a bit for graceful shutdown
126 sleep(Duration::from_millis(100)).await;
127
128 // Force kill if still running
129 let _ = self.process.kill();
130 let _ = self.process.wait();
131 }
132
133 /// Find a free port to use for testing
134 fn find_free_port() -> u16 {
135 use std::net::TcpListener;
136
137 // Bind to port 0 to get a random free port
138 let listener = TcpListener::bind("127.0.0.1:0")
139 .expect("Failed to bind to random port");
140
141 let port = listener.local_addr()
142 .expect("Failed to get local address")
143 .port();
144
145 // Drop the listener to free the port
146 drop(listener);
147
148 port
149 }
150}
151
152impl Drop for TestRelay {
153 fn drop(&mut self) {
154 // Ensure process is killed when TestRelay is dropped
155 let _ = self.process.kill();
156 let _ = self.process.wait();
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[tokio::test]
165 #[ignore] // Requires relay binary to be built
166 async fn test_relay_lifecycle() {
167 let relay = TestRelay::start().await;
168 assert!(relay.url().starts_with("ws://127.0.0.1:"));
169 assert!(relay.port() > 0);
170 relay.stop().await;
171 }
172
173 #[test]
174 fn test_find_free_port() {
175 let port = TestRelay::find_free_port();
176 assert!(port > 0);
177 // Port is u16, so it's always < 65536
178 }
179}