diff --git a/Cargo.lock b/Cargo.lock index 0f5a37b..d6d0fb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2448,7 +2448,11 @@ dependencies = [ name = "test-support" version = "0.1.0" dependencies = [ + "comenqd", + "octocrab", + "tempfile", "tokio", + "wiremock", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 57bf4b2..dfe1eb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = "1.0" thiserror = "1.0" ortho_config = { git = "https://github.com/leynos/ortho-config.git", tag = "v0.4.0" } +tempfile = "3.10" [lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/crates/comenqd/src/daemon.rs b/crates/comenqd/src/daemon.rs index e2095fc..9fdcd1f 100644 --- a/crates/comenqd/src/daemon.rs +++ b/crates/comenqd/src/daemon.rs @@ -2,7 +2,6 @@ //! //! This module provides the run function used by `main` which spawns the //! Unix socket listener and the queue worker. - use crate::config::Config; use anyhow::Result; use comenq_lib::CommentRequest; @@ -17,7 +16,6 @@ use tokio::io::AsyncReadExt; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::{mpsc, watch}; use yaque::{Receiver, Sender, channel}; - fn build_octocrab(token: &str) -> Result { Ok(Octocrab::builder() .personal_token(token.to_string()) @@ -32,7 +30,6 @@ fn prepare_listener(path: &Path) -> Result { stdfs::set_permissions(path, stdfs::Permissions::from_mode(0o660))?; Ok(listener) } - async fn ensure_queue_dir(path: &Path) -> Result<()> { fs::create_dir_all(path).await?; Ok(()) @@ -89,7 +86,6 @@ pub async fn run(config: Config) -> Result<()> { let (client_tx, client_rx) = mpsc::unbounded_channel(); let cfg = Arc::new(config); let (shutdown_tx, shutdown_rx) = watch::channel(()); - let writer = tokio::spawn(queue_writer(queue_tx, client_rx)); let listener = tokio::spawn(run_listener(cfg.clone(), client_tx, shutdown_rx)); let worker = tokio::spawn(run_worker(cfg.clone(), rx, octocrab)); @@ -104,10 +100,8 @@ pub async fn run(config: Config) -> Result<()> { Err(e) => return Err(e.into()), }, } - let _ = shutdown_tx.send(()); writer.await??; - Ok(()) } @@ -252,20 +246,26 @@ mod tests { )); } use tempfile::tempdir; - use test_helpers::{octocrab_for, temp_config}; - use test_support::wait_for_file; + use test_support::{octocrab_for, temp_config, wait_for_file}; use tokio::io::AsyncWriteExt; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::{mpsc, watch}; use tokio::time::{Duration, sleep}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; + fn cfg_with_cooldown(dir: &TempDir, secs: u64) -> Config { + Config { + cooldown_period_seconds: secs, + ..temp_config(dir) + } + } async fn setup_run_worker(status: u16) -> (MockServer, Arc, Receiver, Arc) { let dir = tempdir().expect("tempdir"); - let mut c = temp_config(&dir); - c.cooldown_period_seconds = 0; - let cfg = Arc::new(c); + let cfg = Arc::new(Config { + cooldown_period_seconds: 0, + ..temp_config(&dir) + }); let (sender, rx) = channel(&cfg.queue_path).expect("channel"); let req = CommentRequest { owner: "o".into(), @@ -286,7 +286,6 @@ mod tests { .await; let octo = octocrab_for(&server); - (server, cfg, rx, octo) } @@ -303,14 +302,10 @@ mod tests { #[tokio::test] async fn run_creates_queue_directory() { let dir = tempdir().expect("Failed to create temporary directory"); - let cfg = temp_config(&dir); - + let cfg = cfg_with_cooldown(&dir, 1); assert!(!cfg.queue_path.exists()); - let handle = tokio::spawn(run(cfg.clone())); - wait_for_file(&cfg.queue_path, 200, Duration::from_millis(10)).await; - handle.abort(); assert!(cfg.queue_path.is_dir(), "queue directory not created"); } @@ -323,7 +318,6 @@ mod tests { let listener = prepare_listener(&sock).expect("prepare listener"); drop(listener); - let meta = stdfs::metadata(&sock).expect("metadata"); assert_eq!(meta.permissions().mode() & 0o777, 0o660); } @@ -338,7 +332,6 @@ mod tests { let (mut client, server) = UnixStream::pair().expect("pair"); let handle = tokio::spawn(handle_client(server, client_tx)); - let req = CommentRequest { owner: "o".into(), repo: "r".into(), @@ -350,7 +343,6 @@ mod tests { client.shutdown().await.expect("shutdown"); handle.await.expect("join").expect("client"); drop(writer); // stop queue writer - let guard = receiver.recv().await.expect("recv"); let stored: CommentRequest = serde_json::from_slice(&guard).expect("parse"); assert_eq!(stored, req); @@ -359,17 +351,13 @@ mod tests { #[tokio::test] async fn run_listener_accepts_connections() { let dir = tempdir().expect("tempdir"); - let cfg = Arc::new(temp_config(&dir)); - + let cfg = Arc::new(cfg_with_cooldown(&dir, 1)); let (sender, mut receiver) = channel(&cfg.queue_path).expect("channel"); let (client_tx, writer_rx) = mpsc::unbounded_channel(); let (shutdown_tx, shutdown_rx) = watch::channel(()); let writer = tokio::spawn(queue_writer(sender, writer_rx)); - let listener_task = tokio::spawn(run_listener(cfg.clone(), client_tx, shutdown_rx)); - wait_for_file(&cfg.socket_path, 10, Duration::from_millis(10)).await; - let mut stream = UnixStream::connect(&cfg.socket_path) .await .expect("connect"); @@ -382,11 +370,9 @@ mod tests { let payload = serde_json::to_vec(&req).expect("serialize"); stream.write_all(&payload).await.expect("write"); stream.shutdown().await.expect("shutdown"); - let guard = receiver.recv().await.expect("recv"); let stored: CommentRequest = serde_json::from_slice(&guard).expect("parse"); assert_eq!(stored, req); - listener_task.abort(); let _ = shutdown_tx.send(()); drop(writer); @@ -398,7 +384,6 @@ mod tests { let h = tokio::spawn(run_worker(cfg.clone(), rx, octo)); sleep(Duration::from_millis(50)).await; h.abort(); - assert_eq!(server.received_requests().await.unwrap().len(), 1); assert_eq!(std::fs::read_dir(&cfg.queue_path).unwrap().count(), 0); } @@ -409,7 +394,6 @@ mod tests { let h = tokio::spawn(run_worker(cfg.clone(), rx, octo)); sleep(Duration::from_millis(50)).await; h.abort(); - assert_eq!(server.received_requests().await.unwrap().len(), 1); assert!(std::fs::read_dir(&cfg.queue_path).unwrap().count() > 0); } diff --git a/test-support/Cargo.toml b/test-support/Cargo.toml index d8fdef4..ac68fa2 100644 --- a/test-support/Cargo.toml +++ b/test-support/Cargo.toml @@ -5,3 +5,7 @@ edition = "2024" [dependencies] tokio = { workspace = true } +comenqd = { path = "../crates/comenqd" } +octocrab = { workspace = true } +tempfile = { workspace = true } +wiremock = "^0.6" diff --git a/test-support/src/daemon.rs b/test-support/src/daemon.rs new file mode 100644 index 0000000..c6fa395 --- /dev/null +++ b/test-support/src/daemon.rs @@ -0,0 +1,40 @@ +//! Helper utilities for daemon tests. +//! +//! Provides constructors for temporary daemon [`Config`]s and simplified +//! creation of [`Octocrab`] clients targeting a [`MockServer`]. + +#![expect(clippy::expect_used, reason = "simplify test setup")] + +use std::sync::Arc; + +use comenqd::config::Config; +use octocrab::Octocrab; +use tempfile::TempDir; +use wiremock::MockServer; + +/// Build a [`Config`] using paths inside `tmp`. +/// +/// The configuration uses a dummy GitHub token and a one second cooldown. +pub fn temp_config(tmp: &TempDir) -> Config { + Config { + github_token: "t".into(), + socket_path: tmp.path().join("sock"), + queue_path: tmp.path().join("q"), + cooldown_period_seconds: 1, + } +} + +/// Construct an [`Octocrab`] client for a [`MockServer`]. +/// +/// The client is initialised with a placeholder token and its base URL +/// configured to the mock server's URI. +pub fn octocrab_for(server: &MockServer) -> Arc { + Arc::new( + Octocrab::builder() + .personal_token("t".to_string()) + .base_uri(server.uri()) + .expect("base_uri") + .build() + .expect("build octocrab"), + ) +} diff --git a/test-support/src/lib.rs b/test-support/src/lib.rs index 3775971..71ee044 100644 --- a/test-support/src/lib.rs +++ b/test-support/src/lib.rs @@ -1,5 +1,7 @@ //! Test support utilities. +pub mod daemon; pub mod util; +pub use daemon::{octocrab_for, temp_config}; pub use util::wait_for_file; diff --git a/tests/cucumber.rs b/tests/cucumber.rs index b1f8f32..fc22ddd 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -1,6 +1,6 @@ mod steps; -mod util; mod support; +mod util; use cucumber::World as _; use steps::{ CliWorld, ClientWorld, CommentWorld, ConfigWorld, ListenerWorld, PackagingWorld, WorkerWorld, diff --git a/tests/steps/listener_steps.rs b/tests/steps/listener_steps.rs index 98c73da..bb2d195 100644 --- a/tests/steps/listener_steps.rs +++ b/tests/steps/listener_steps.rs @@ -12,7 +12,7 @@ use tokio::io::AsyncWriteExt; use tokio::net::UnixStream; use tokio::sync::{mpsc, watch}; -use crate::util::test_helpers::temp_config; +use crate::util::temp_config; use comenq_lib::CommentRequest; use comenqd::config::Config; use comenqd::daemon::{queue_writer, run_listener}; diff --git a/tests/steps/worker_steps.rs b/tests/steps/worker_steps.rs index 517304a..250b216 100644 --- a/tests/steps/worker_steps.rs +++ b/tests/steps/worker_steps.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use std::time::Duration; -use crate::util::test_helpers::{octocrab_for, temp_config}; +use crate::util::{octocrab_for, temp_config}; use comenq_lib::CommentRequest; use comenqd::config::Config; use comenqd::daemon::run_worker; @@ -36,9 +36,9 @@ impl std::fmt::Debug for WorkerWorld { #[given("a queued comment request")] async fn queued_request(world: &mut WorkerWorld) { let dir = TempDir::new().expect("tempdir"); - let mut cfg = temp_config(&dir); - cfg.cooldown_period_seconds = 0; - let cfg = Arc::new(cfg); + let mut base = temp_config(&dir); + base.cooldown_period_seconds = 0; + let cfg = Arc::new(base); let (mut sender, receiver) = channel(&cfg.queue_path).expect("channel"); let req = CommentRequest { owner: "o".into(), diff --git a/tests/util/mod.rs b/tests/util/mod.rs index a5b0ca8..0de4bac 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -1,3 +1,3 @@ -//! Test utility modules. +//! Re-exports of common test helpers. -pub mod test_helpers; +pub use test_support::{octocrab_for, temp_config};