From c1d1ff14a5d0806b9506fe709363a88acd1e713d Mon Sep 17 00:00:00 2001 From: gramps Date: Thu, 2 Apr 2026 19:32:24 -0700 Subject: [PATCH] Add RabbitMQ validation, test scaffolding, and config refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract ipl() from main() with env-aware error handling (fatal in prod, warn in dev) - Add amqp::validate() — TCP reachability check for RabbitMQ at IPL - Refactor config::load() into load() + load_from() for testability - Add lib.rs to expose public API to integration test harness - Add test fixture scaffolding: tests/fixtures/beds_test.toml, tests/common/mod.rs - Add unit tests for amqp::validate() error paths (closed port, bad address) Co-Authored-By: Claude Sonnet 4.6 --- src/amqp.rs | 86 +++++++++++++++++++++++++++++++++++ src/config/mod.rs | 84 ++++++++++++++++++++++++++++++---- src/lib.rs | 18 ++++++++ src/main.rs | 76 +++++++++++++++++++++++++++++-- tests/common/mod.rs | 39 ++++++++++++++++ tests/fixtures/beds_test.toml | 51 +++++++++++++++++++++ 6 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 src/amqp.rs create mode 100644 src/lib.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/fixtures/beds_test.toml diff --git a/src/amqp.rs b/src/amqp.rs new file mode 100644 index 0000000..814b355 --- /dev/null +++ b/src/amqp.rs @@ -0,0 +1,86 @@ +//! # amqp.rs — RabbitMQ Transport Layer +//! +//! Manages all AMQP interactions for the BEDS node. At IPL, validates that +//! the RabbitMQ broker is reachable before the node proceeds. Future phases +//! will add channel acquisition, queue declaration, and message dispatch. +//! +//! ## Calling Agents +//! - `ipl()` in main.rs — calls `validate()` during the IPL sequence +//! +//! ## Inputs +//! - `BrokerServicesConfig` from the loaded BEDS configuration +//! +//! ## Outputs +//! - `Ok(())` if the broker is reachable +//! - `Err(String)` with host:port and OS error if the broker cannot be reached +//! +//! **Author:** mks +//! **Version:** 1.0 +//! +//! ## History +//! * `2026-04-02` - mks - original coding + +use std::net::TcpStream; +use std::time::Duration; + +use crate::config::BrokerServicesConfig; + +/// Validates that the RabbitMQ broker is reachable. +/// +/// Opens a TCP connection to the configured broker host and port. Does not +/// authenticate or open an AMQP channel — reachability only. The connection +/// is closed immediately after a successful connect. +/// +/// # Arguments +/// +/// * `cfg` — broker services configuration block from `BedsConfig` +/// +/// # Returns +/// +/// `Ok(())` if the TCP handshake succeeds. +/// `Err(String)` with a descriptive message if the broker cannot be reached +/// or if the configured address is malformed. +/// +/// # History +/// +/// * `2026-04-02` - mks - original coding +pub fn validate(cfg: &BrokerServicesConfig) -> Result<(), String> { + let addr_str = format!("{}:{}", cfg.app_server.host, cfg.app_server.port); + + let addr: std::net::SocketAddr = addr_str + .parse() + .map_err(|e| format!("Invalid broker address {}: {}", addr_str, e))?; + + TcpStream::connect_timeout(&addr, Duration::from_secs(5)) + .map_err(|e| format!("RabbitMQ unreachable at {}: {}", addr_str, e))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::load_from; + + /// Loads the test fixture config. Panics if the fixture is missing or malformed. + fn test_cfg() -> crate::config::BedsConfig { + load_from("tests/fixtures/beds_test.toml", "") + .expect("test fixture beds_test.toml failed to load") + } + + #[test] + fn validate_err_on_closed_port() { + // port 1 is reserved and always closed — guarantees a connection refusal + // without requiring any live service + let mut cfg = test_cfg(); + cfg.broker_services.app_server.port = 1; + assert!(validate(&cfg.broker_services).is_err()); + } + + #[test] + fn validate_err_on_bad_address() { + let mut cfg = test_cfg(); + cfg.broker_services.app_server.host = "not_a_valid_host!!!".to_string(); + assert!(validate(&cfg.broker_services).is_err()); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 94b23e4..2e65530 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,5 +1,32 @@ +//! # config/mod.rs — BEDS Configuration Loader +//! +//! Loads and merges the BEDS configuration from TOML files. The base file +//! (`beds.toml`) holds production-safe defaults; an optional env override +//! file (`env_{name}.toml`) is layered on top. Values in the env file take +//! precedence over the base. +//! +//! ## Calling Agents +//! - `ipl()` in main.rs — calls `load()` as the first IPL step +//! - `tests/common/mod.rs` — calls `load_from()` directly with fixture paths +//! +//! ## Inputs +//! - `BEDS_ENV` environment variable selects the env override file (default: dev) +//! - `base_path`: path to the base config TOML +//! - `env_path`: path to the env override TOML (empty string = no override) +//! +//! ## Outputs +//! - `BedsConfig` — fully-merged, deserialised configuration struct +//! - `ConfigError` on parse or deserialisation failure +//! +//! **Author:** mks +//! **Version:** 1.0 +//! +//! ## History +//! * `2026-04-02` - mks - original coding +//! * `2026-04-02` - mks - refactored into load() + load_from() for testability + mod structs; -pub use structs::BedsConfig; +pub use structs::{BedsConfig, BrokerServicesConfig}; use config::{Config, File, FileFormat}; @@ -9,14 +36,55 @@ pub enum ConfigError { LoadError(#[from] config::ConfigError), } +/// Loads BEDS configuration from explicit file paths. +/// +/// Builds the config from a required base file and an optional env override +/// file. If `env_path` is an empty string, no override is applied. This +/// function is the single point of config construction — `load()` delegates +/// to it with the standard production paths. +/// +/// # Arguments +/// +/// * `base_path` — path to the base TOML config file (required) +/// * `env_path` — path to the env override TOML file; empty string skips it +/// +/// # Returns +/// +/// `Ok(BedsConfig)` on success. +/// `Err(ConfigError)` if the base file is missing, malformed, or +/// deserialisation fails. +/// +/// # History +/// +/// * `2026-04-02` - mks - original coding +pub fn load_from(base_path: &str, env_path: &str) -> Result { + let mut builder = Config::builder() + .add_source(File::new(base_path, FileFormat::Toml)); + + if !env_path.is_empty() { + builder = builder.add_source(File::new(env_path, FileFormat::Toml).required(false)); + } + + Ok(builder.build()?.try_deserialize()?) +} + +/// Loads BEDS configuration using the standard production paths. +/// +/// Reads `config/beds.toml` as the base, then layers +/// `config/env_{BEDS_ENV}.toml` on top (default env: `dev`). +/// Delegates to `load_from()`. +/// +/// # Returns +/// +/// `Ok(BedsConfig)` on success. +/// `Err(ConfigError)` if the base file is missing, malformed, or +/// deserialisation fails. +/// +/// # History +/// +/// * `2026-04-02` - mks - original coding pub fn load() -> Result { let env = std::env::var("BEDS_ENV").unwrap_or_else(|_| "dev".to_string()); let env_file = format!("config/env_{}.toml", env); - - let cfg = Config::builder() - .add_source(File::new("config/beds.toml", FileFormat::Toml)) - .add_source(File::new(&env_file, FileFormat::Toml).required(false)) - .build()?; - - Ok(cfg.try_deserialize()?) + load_from("config/beds.toml", &env_file) } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e0454cd --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,18 @@ +//! # lib.rs — BEDS Public API +//! +//! Exposes BEDS modules for use by the integration test harness. All +//! application logic lives in the individual modules; this file re-exports +//! what tests need to reach. +//! +//! ## Calling Agents +//! - `tests/common/mod.rs` and all files under `tests/` — via `rustybeds::` +//! +//! **Author:** mks +//! **Version:** 1.0 +//! +//! ## History +//! * `2026-04-02` - mks - original coding + +pub mod amqp; +pub mod config; +pub mod logging; diff --git a/src/main.rs b/src/main.rs index 4abe38a..833dbc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,80 @@ +//! # main.rs — BEDS Entry Point +//! +//! Application entry point. Invokes the IPL (Initial Program Load) sequence +//! which bootstraps all required services before the node enters its +//! operational state. +//! +//! ## Calling Agents +//! None — this is the process entry point. +//! +//! ## Inputs +//! - `BEDS_ENV` environment variable selects the env config file (default: dev) +//! +//! ## Outputs +//! - Running BEDS node, blocked on signal handler (not yet implemented) +//! - On fatal IPL failure: error output to console, process exits non-zero +//! +//! **Author:** ms +//! **Version:** 1.0 +//! +//! ## History +//! * `2026-04-02` - mks - original coding +//! * `2026-04-02` - mks - refactored startup sequence into ipl() + +mod amqp; mod config; mod logging; -fn main() { - let cfg = config::load().expect("Failed to load config"); +/// Executes the BEDS Initial Program Load (IPL) sequence. +/// +/// IPL bootstraps the node in strict order. Each step must succeed before +/// the next is attempted. In production environments, any failure is fatal +/// and the process exits with a console error report. In development +/// environments, non-critical failures are logged and IPL continues. +/// +/// ## IPL Sequence +/// 1. Load configuration (beds.toml + env override) +/// 2. Initialize logging +/// 3. Connect to required services (AMQP, store adapters) — not yet implemented +/// 4. Declare queues based on node role — not yet implemented +/// 5. Node green +/// +/// # Returns +/// +/// `Ok(())` if all required services are available and the node is ready. +/// `Err(String)` with a descriptive error message if IPL cannot complete. +/// +/// # History +/// +/// * `2026-04-02` - mks - original coding +fn ipl() -> Result<(), String> { + // load configuration — fatal in all environments if this fails + let cfg = config::load().map_err(|e| format!("Failed to load config: {}", e))?; + // initialize logging — must come before any tracing calls logging::init_from_config(cfg.syslog, cfg.syslog_mirror_console); - tracing::info!("BEDS starting, env={}", cfg.id.env_name); + + tracing::info!("BEDS IPL starting, node={} env={}", cfg.id.wbid, cfg.id.env_name); + tracing::info!("Configuration loaded"); tracing::info!("Logging initialized"); + + // validate broker reachability — fatal in production, non-fatal in all other envs + match amqp::validate(&cfg.broker_services) { + Ok(()) => tracing::info!("RabbitMQ reachable"), + Err(e) => { + if cfg.id.env_name == "production" { + return Err(e); + } + tracing::warn!("RabbitMQ unreachable (non-fatal in {}): {}", cfg.id.env_name, e); + } + } + + Ok(()) +} + +fn main() { + if let Err(e) = ipl() { + eprintln!("[BEDS] [FATAL] [IPL] {}", e); + std::process::exit(1); + } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..bce382c --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,39 @@ +//! # tests/common/mod.rs — Shared Test Helpers +//! +//! Provides shared fixtures and helper functions for all BEDS integration +//! tests. Unit tests within source files use the same fixture path directly +//! via `config::load_from()`. +//! +//! ## Usage +//! +//! In any integration test file under `tests/`: +//! ``` +//! mod common; +//! let cfg = common::load_test_config(); +//! ``` +//! +//! **Author:** mks +//! **Version:** 1.0 +//! +//! ## History +//! * `2026-04-02` - mks - original coding + +/// Path to the canonical test config fixture, relative to the workspace root. +pub const TEST_CONFIG_PATH: &str = "tests/fixtures/beds_test.toml"; + +/// Loads the canonical BEDS test configuration fixture. +/// +/// Panics on failure — a missing or malformed test fixture is a broken test +/// environment, not a recoverable error. +/// +/// # Returns +/// +/// A fully-deserialised `BedsConfig` from `tests/fixtures/beds_test.toml`. +/// +/// # History +/// +/// * `2026-04-02` - mks - original coding +pub fn load_test_config() -> rustybeds::config::BedsConfig { + rustybeds::config::load_from(TEST_CONFIG_PATH, "") + .expect("test fixture beds_test.toml failed to load") +} diff --git a/tests/fixtures/beds_test.toml b/tests/fixtures/beds_test.toml new file mode 100644 index 0000000..5005211 --- /dev/null +++ b/tests/fixtures/beds_test.toml @@ -0,0 +1,51 @@ +# ============================================================================= +# BEDS Test Fixture — beds_test.toml +# ============================================================================= +# +# Canonical configuration fixture for all BEDS unit and integration tests. +# Values here are test-safe: localhost services, known ports, no real creds. +# +# Load via tests/common/mod.rs::load_test_config() — do not call +# config::load() directly from tests, as that reads the live config/beds.toml. +# +# AUTHOR: mks +# VERSION: 1.0 +# +# HISTORY: +# ======== +# 2026-04-02 mks original coding +# ============================================================================= + +debug = true +syslog = false +syslog_mirror_console = true +audit_on = false +journal_on = false + +[id] +env_name = "dev" +version = "1.0" +wbid = "ms" + +[broker_services] +queue_tag = "test_" +vhost = "test" +timer_violation = 3000 +records_per_xfer = 5000 +keepalive = true +heartbeat = 60 +use_ssl = false +cert_path = "/etc/rabbitmq" + +[broker_services.app_server] +host = "127.0.0.1" +port = 5672 +api_port = 15672 +user = "beds" +pass = "changeme" +rpi = 50 + +[broker_services.app_server.instances] +r_broker = 2 +w_broker = 2 +m_broker = 0