Add RabbitMQ validation, test scaffolding, and config refactor
- 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 <noreply@anthropic.com>
This commit is contained in:
86
src/amqp.rs
Normal file
86
src/amqp.rs
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<BedsConfig, ConfigError> {
|
||||
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<BedsConfig, ConfigError> {
|
||||
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)
|
||||
}
|
||||
|
||||
18
src/lib.rs
Normal file
18
src/lib.rs
Normal file
@@ -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;
|
||||
76
src/main.rs
76
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);
|
||||
}
|
||||
}
|
||||
|
||||
39
tests/common/mod.rs
Normal file
39
tests/common/mod.rs
Normal file
@@ -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")
|
||||
}
|
||||
51
tests/fixtures/beds_test.toml
vendored
Normal file
51
tests/fixtures/beds_test.toml
vendored
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user