use tokio::sync::mpsc;
use super::types::{RemoteChange, RemoteLogEntry, RemoteProject};
pub async fn spawn_mock_ws_server(messages: Vec<String>) -> std::net::SocketAddr {
use tokio::net::TcpListener;
use tokio_tungstenite::{accept_async, tungstenite::Message};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
if let Ok((stream, _)) = listener.accept().await {
if let Ok(mut ws) = accept_async(stream).await {
use futures_util::SinkExt;
for msg in messages {
let _ = ws.send(Message::Text(msg)).await;
}
let _ = ws.close(None).await;
}
}
});
addr
}
pub async fn spawn_mock_http_server(
) -> (std::net::SocketAddr, tokio::sync::oneshot::Receiver<String>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
if let Ok((mut stream, _)) = listener.accept().await {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await.unwrap_or(0);
let request_text = String::from_utf8_lossy(&buf[..n]).to_string();
let _ = tx.send(request_text);
let response =
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n[]";
let _ = stream.write_all(response.as_bytes()).await;
}
});
(addr, rx)
}
pub fn remote_change_json(
id: &str,
project: &str,
completed_tasks: u32,
total_tasks: u32,
status: &str,
iteration_number: Option<u32>,
) -> String {
let iter = match iteration_number {
Some(n) => n.to_string(),
None => "null".to_string(),
};
format!(
r#"{{
"id": "{id}",
"project": "{project}",
"completed_tasks": {completed_tasks},
"total_tasks": {total_tasks},
"last_modified": "2024-01-01T00:00:00Z",
"status": "{status}",
"iteration_number": {iter},
"selected": true
}}"#
)
}
pub fn full_state_json(project_id: &str, project_name: &str, changes_json: &[String]) -> String {
let changes = changes_json.join(",\n");
format!(
r#"{{
"type": "full_state",
"projects": [
{{
"id": "{project_id}",
"name": "{project_name}",
"repo": "{project_name}",
"branch": "main",
"status": "idle",
"is_busy": false,
"error": null,
"sync_state": "up_to_date",
"ahead_count": 0,
"behind_count": 0,
"sync_required": false,
"local_sha": null,
"remote_sha": null,
"last_remote_check_at": null,
"remote_check_error": null,
"changes": [{changes}]
}}
],
"sync_available": false
}}"#
)
}
pub fn change_update_json(change_json: &str) -> String {
format!(
r#"{{
"type": "change_update",
"change": {change_json}
}}"#
)
}
pub fn log_message_json(
message: &str,
level: &str,
change_id: Option<&str>,
timestamp: &str,
) -> String {
let change_id_json = match change_id {
Some(id) => format!(r#""{id}""#),
None => "null".to_string(),
};
format!(
r#"{{
"type": "log",
"entry": {{
"message": "{message}",
"level": "{level}",
"change_id": {change_id_json},
"timestamp": "{timestamp}"
}}
}}"#
)
}
pub fn make_remote_change(id: &str, project: &str) -> RemoteChange {
RemoteChange {
id: id.to_string(),
project: project.to_string(),
completed_tasks: 0,
total_tasks: 1,
last_modified: "2024-01-01T00:00:00Z".to_string(),
status: "queued".to_string(),
iteration_number: None,
selected: true,
}
}
pub fn make_remote_project(id: &str, name: &str, changes: Vec<RemoteChange>) -> RemoteProject {
RemoteProject {
id: id.to_string(),
name: name.to_string(),
repo: name.to_string(),
branch: "main".to_string(),
status: "idle".to_string(),
is_busy: false,
error: None,
sync_state: "up_to_date".to_string(),
ahead_count: 0,
behind_count: 0,
sync_required: false,
local_sha: None,
remote_sha: None,
last_remote_check_at: None,
remote_check_error: None,
changes,
}
}
pub fn make_remote_log_entry(message: &str, level: &str) -> RemoteLogEntry {
RemoteLogEntry {
message: message.to_string(),
level: level.to_string(),
change_id: None,
timestamp: "2024-01-01T00:00:00Z".to_string(),
project_id: None,
operation: None,
iteration: None,
}
}
#[derive(Debug)]
pub struct CapturedRequest {
pub raw: String,
pub method: String,
pub path: String,
pub body: String,
}
pub async fn spawn_flexible_mock_http_server(
response_body: String,
) -> (
std::net::SocketAddr,
tokio::sync::oneshot::Receiver<CapturedRequest>,
) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
if let Ok((mut stream, _)) = listener.accept().await {
let mut buf = [0u8; 8192];
let n = stream.read(&mut buf).await.unwrap_or(0);
let request_text = String::from_utf8_lossy(&buf[..n]).to_string();
let first_line = request_text.lines().next().unwrap_or("");
let parts: Vec<&str> = first_line.split_whitespace().collect();
let method = parts.first().copied().unwrap_or("").to_string();
let path = parts.get(1).copied().unwrap_or("").to_string();
let body = if let Some(idx) = request_text.find("\r\n\r\n") {
request_text[idx + 4..].to_string()
} else {
String::new()
};
let captured = CapturedRequest {
raw: request_text.to_lowercase(),
method,
path,
body,
};
let _ = tx.send(captured);
let content_len = response_body.len();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
content_len, response_body
);
let _ = stream.write_all(response.as_bytes()).await;
}
});
(addr, rx)
}
pub async fn spawn_ws_header_capture_server(
) -> (std::net::SocketAddr, tokio::sync::mpsc::Receiver<String>) {
use tokio::io::AsyncReadExt;
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = tokio::sync::mpsc::channel::<String>(1);
tokio::spawn(async move {
if let Ok((mut stream, _)) = listener.accept().await {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await.unwrap_or(0);
let request_text = String::from_utf8_lossy(&buf[..n]).to_lowercase();
let _ = tx.send(request_text).await;
}
});
(addr, rx)
}
pub async fn spawn_mock_http_server_ordered(
responses: Vec<(u16, String)>,
) -> (std::net::SocketAddr, tokio::sync::mpsc::Receiver<String>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let count = responses.len();
let (tx, rx) = tokio::sync::mpsc::channel::<String>(count + 1);
tokio::spawn(async move {
for (status, body) in responses {
if let Ok((mut stream, _)) = listener.accept().await {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await.unwrap_or(0);
let request_text = String::from_utf8_lossy(&buf[..n]).to_string();
let method_path = request_text
.lines()
.next()
.and_then(|line| {
let parts: Vec<&str> = line.splitn(3, ' ').collect();
if parts.len() >= 2 {
Some(format!("{} {}", parts[0], parts[1]))
} else {
None
}
})
.unwrap_or_default();
let _ = tx.send(method_path).await;
let reason = if status == 200 {
"OK"
} else {
"Internal Server Error"
};
let content_len = body.len();
let response = format!(
"HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {content_len}\r\nConnection: close\r\n\r\n{body}"
);
let _ = stream.write_all(response.as_bytes()).await;
}
}
});
(addr, rx)
}
pub async fn recv_with_timeout<T: std::fmt::Debug>(
rx: &mut mpsc::Receiver<T>,
timeout_secs: u64,
context: &str,
) -> T {
tokio::time::timeout(tokio::time::Duration::from_secs(timeout_secs), rx.recv())
.await
.unwrap_or_else(|_| panic!("Timed out waiting for {} message", context))
.unwrap_or_else(|| panic!("Channel closed before receiving {} message", context))
}