v0.3.2-pre.005
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-store-postgres-lib/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-store-postgres-lib"
|
||||
@@ -8,7 +8,14 @@ edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
deadpool-postgres = { workspace = true, features = ["rt_tokio_1"] }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-store-api = { path = "../ksp-store-api" }
|
||||
rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] }
|
||||
rustls-native-certs.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "time"] }
|
||||
tokio-postgres = { workspace = true, features = ["runtime"] }
|
||||
tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
45
crates/ksp-store-postgres-lib/src/error.rs
Normal file
45
crates/ksp-store-postgres-lib/src/error.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// Safe backend-local classification used by the Store facade for stable error mapping.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostgresBackendErrorKind {
|
||||
/// The supplied physical PostgreSQL configuration is unsupported or malformed.
|
||||
ConfigInvalid,
|
||||
/// A physical PostgreSQL connection could not be established.
|
||||
ConnectFailed,
|
||||
/// A bounded pool wait, create or recycle operation reached its deadline.
|
||||
PoolTimeout,
|
||||
/// Explicit backend shutdown did not drain inside the supplied deadline.
|
||||
ShutdownTimeout,
|
||||
/// Verified TLS configuration or negotiation could not be established.
|
||||
TlsFailed,
|
||||
}
|
||||
|
||||
/// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresBackendError {
|
||||
kind: PostgresBackendErrorKind,
|
||||
phase: &'static str,
|
||||
}
|
||||
|
||||
impl PostgresBackendError {
|
||||
/// Creates one backend error without retaining external error text or sensitive connection material.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(kind: PostgresBackendErrorKind, phase: &'static str) -> Self {
|
||||
return Self { kind, phase };
|
||||
}
|
||||
|
||||
/// Returns the safe backend-local error classification.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> PostgresBackendErrorKind {
|
||||
return self.kind;
|
||||
}
|
||||
|
||||
/// Returns the static safe lifecycle phase associated with the failure.
|
||||
#[must_use]
|
||||
pub const fn phase(&self) -> &'static str {
|
||||
return self.phase;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,36 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Official PostgreSQL backend implementation scaffold for KSP Store.
|
||||
//! Official PostgreSQL backend implementation for KSP Store.
|
||||
//!
|
||||
//! `0.3.2-pre.002` establishes only the backend crate boundary. PostgreSQL
|
||||
//! driver, pooling, TLS, SQL, migrations and health behavior are intentionally
|
||||
//! absent until their dedicated prereleases.
|
||||
//! `0.3.2-pre.005` owns the physical `tokio-postgres` connection, bounded
|
||||
//! Deadpool pool and explicit Rustls TLS policy. SQL migrations and business
|
||||
//! persistence remain absent until their dedicated prereleases.
|
||||
//!
|
||||
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`, which
|
||||
//! keeps backend implementation ownership acyclic and reusable behind the
|
||||
//! common facade.
|
||||
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
|
||||
//! common facade consumes only this crate's narrow backend bridge and never
|
||||
//! exposes PostgreSQL pool, client, row or statement types.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
mod runtime;
|
||||
|
||||
/// Crate-owned tracing target reserved for later PostgreSQL backend behavior.
|
||||
/// Safe backend-local error returned to the common Store facade.
|
||||
pub use self::error::PostgresBackendError;
|
||||
/// Safe backend-local error classification used by the common Store facade.
|
||||
pub use self::error::PostgresBackendErrorKind;
|
||||
/// Opaque physical PostgreSQL backend owning its connection pool.
|
||||
pub use self::runtime::PostgresBackend;
|
||||
/// Physical PostgreSQL settings bridge consumed only by the backend crate.
|
||||
pub use self::runtime::PostgresBackendSettings;
|
||||
/// TLS mode accepted by the physical PostgreSQL settings bridge.
|
||||
pub use self::runtime::PostgresBackendTlsMode;
|
||||
|
||||
/// Crate-owned tracing target for PostgreSQL backend behavior.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
// Keep the mandatory crate-owned tracing target part of the compiled scaffold
|
||||
// without inventing runtime logging before the first behavioral tranche.
|
||||
const _: &str = TRACING_TARGET;
|
||||
const _: &str = crate::TRACING_TARGET;
|
||||
|
||||
303
crates/ksp-store-postgres-lib/src/runtime.rs
Normal file
303
crates/ksp-store-postgres-lib/src/runtime.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||
// version: 1
|
||||
|
||||
const APPLICATION_NAME: &str = "ksp-store";
|
||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||
const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10;
|
||||
|
||||
/// TLS mode accepted by the physical PostgreSQL backend bridge.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostgresBackendTlsMode {
|
||||
/// Disable TLS for the selected PostgreSQL target.
|
||||
Disabled,
|
||||
/// Require TLS with system-root trust and server-identity verification.
|
||||
VerifyFull,
|
||||
}
|
||||
|
||||
impl PostgresBackendTlsMode {
|
||||
/// Returns the stable safe TLS mode code used only in diagnostics.
|
||||
#[must_use]
|
||||
pub const fn code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::Disabled => "disabled",
|
||||
Self::VerifyFull => "verify_full",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical settings consumed only by the PostgreSQL backend crate.
|
||||
pub struct PostgresBackendSettings {
|
||||
connect_timeout: std::time::Duration,
|
||||
connection_uri: std::string::String,
|
||||
create_timeout: std::time::Duration,
|
||||
max_connections: u32,
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
recycle_timeout: std::time::Duration,
|
||||
tls_mode: PostgresBackendTlsMode,
|
||||
wait_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl PostgresBackendSettings {
|
||||
/// Creates the physical PostgreSQL settings bridge from already validated facade-owned values.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
connection_uri: impl std::convert::Into<std::string::String>,
|
||||
max_connections: u32,
|
||||
connect_timeout: std::time::Duration,
|
||||
wait_timeout: std::time::Duration,
|
||||
create_timeout: std::time::Duration,
|
||||
recycle_timeout: std::time::Duration,
|
||||
tls_mode: PostgresBackendTlsMode,
|
||||
) -> Self {
|
||||
return Self {
|
||||
connect_timeout,
|
||||
connection_uri: connection_uri.into(),
|
||||
create_timeout,
|
||||
max_connections,
|
||||
network,
|
||||
recycle_timeout,
|
||||
tls_mode,
|
||||
wait_timeout,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the logical network bound to this one backend instance.
|
||||
#[must_use]
|
||||
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
|
||||
return &self.network;
|
||||
}
|
||||
|
||||
/// Returns the selected safe TLS mode.
|
||||
#[must_use]
|
||||
pub const fn tls_mode(&self) -> PostgresBackendTlsMode {
|
||||
return self.tls_mode;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostgresBackendSettings {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("PostgresBackendSettings")
|
||||
.field("network", &self.network)
|
||||
.field("connection_uri", &"<redacted>")
|
||||
.field("max_connections", &self.max_connections)
|
||||
.field("connect_timeout", &self.connect_timeout)
|
||||
.field("wait_timeout", &self.wait_timeout)
|
||||
.field("create_timeout", &self.create_timeout)
|
||||
.field("recycle_timeout", &self.recycle_timeout)
|
||||
.field("tls_mode", &self.tls_mode)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque physical PostgreSQL backend owning the bounded Deadpool connection pool.
|
||||
pub struct PostgresBackend {
|
||||
network: ksp_store_api::RawNetworkId,
|
||||
pool: deadpool_postgres::Pool,
|
||||
}
|
||||
|
||||
impl PostgresBackend {
|
||||
/// Parses and normalizes one supplied URI, builds a bounded pool and proves one physical connection before returning readiness.
|
||||
pub async fn open(settings: PostgresBackendSettings) -> std::result::Result<Self, crate::PostgresBackendError> {
|
||||
let normalized = normalized_config(&settings);
|
||||
let pg_config = match normalized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
network = settings.network().as_str(),
|
||||
tls_mode = settings.tls_mode().code(),
|
||||
max_connections = settings.max_connections,
|
||||
"opening PostgreSQL Store backend pool"
|
||||
);
|
||||
let pool_result = match settings.tls_mode {
|
||||
PostgresBackendTlsMode::Disabled => build_pool(pg_config, tokio_postgres::NoTls, &settings),
|
||||
PostgresBackendTlsMode::VerifyFull => {
|
||||
let tls_result = build_verified_tls();
|
||||
let tls = match tls_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
build_pool(pg_config, tls, &settings)
|
||||
},
|
||||
};
|
||||
let pool = match pool_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let probe = pool.get().await;
|
||||
match probe {
|
||||
std::result::Result::Ok(client) => drop(client),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
network = settings.network().as_str(),
|
||||
tls_mode = settings.tls_mode().code(),
|
||||
"PostgreSQL Store backend established initial physical connection"
|
||||
);
|
||||
return std::result::Result::Ok(Self { network: settings.network, pool });
|
||||
}
|
||||
|
||||
/// Explicitly closes the pool and waits for all owned pooled objects to drain inside the supplied bound.
|
||||
pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
self.pool.close();
|
||||
let drain = async {
|
||||
loop {
|
||||
if self.pool.status().size == 0 {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_POLL_INTERVAL_MS)).await;
|
||||
}
|
||||
};
|
||||
let result = tokio::time::timeout(timeout, drain).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(()) => {
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, network = self.network.as_str(), "PostgreSQL Store backend pool closed");
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(_) => {
|
||||
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ShutdownTimeout, "pool_drain"))
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostgresBackend {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("PostgresBackend").field("network", &self.network).field("state", &"open").finish();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for PostgresBackend {
|
||||
fn drop(&mut self) {
|
||||
self.pool.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_config(settings: &PostgresBackendSettings) -> std::result::Result<tokio_postgres::Config, crate::PostgresBackendError> {
|
||||
if settings.connection_uri.is_empty() || settings.connection_uri.len() > MAX_CONNECTION_URI_BYTES {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
|
||||
}
|
||||
let parsed = settings.connection_uri.parse::<tokio_postgres::Config>();
|
||||
let mut config = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
|
||||
},
|
||||
};
|
||||
if config.get_options().is_some() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "server_options"));
|
||||
}
|
||||
if config.get_hosts().is_empty() && config.get_hostaddrs().is_empty() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "host"));
|
||||
}
|
||||
if settings.tls_mode == PostgresBackendTlsMode::VerifyFull {
|
||||
if config.get_hosts().is_empty() {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
|
||||
}
|
||||
for host in config.get_hosts() {
|
||||
if !is_tcp_host(host) {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
|
||||
}
|
||||
}
|
||||
}
|
||||
config.application_name(APPLICATION_NAME);
|
||||
config.connect_timeout(settings.connect_timeout);
|
||||
config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres);
|
||||
match settings.tls_mode {
|
||||
PostgresBackendTlsMode::Disabled => {
|
||||
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
|
||||
},
|
||||
PostgresBackendTlsMode::VerifyFull => {
|
||||
config.ssl_mode(tokio_postgres::config::SslMode::Require);
|
||||
},
|
||||
}
|
||||
return std::result::Result::Ok(config);
|
||||
}
|
||||
|
||||
fn is_tcp_host(host: &tokio_postgres::config::Host) -> bool {
|
||||
return match host {
|
||||
tokio_postgres::config::Host::Tcp(_) => true,
|
||||
#[cfg(unix)]
|
||||
tokio_postgres::config::Host::Unix(_) => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn build_pool<T>(
|
||||
pg_config: tokio_postgres::Config,
|
||||
tls: T,
|
||||
settings: &PostgresBackendSettings,
|
||||
) -> std::result::Result<deadpool_postgres::Pool, crate::PostgresBackendError>
|
||||
where
|
||||
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + std::clone::Clone + std::marker::Send + std::marker::Sync + 'static,
|
||||
T::Stream: std::marker::Send + std::marker::Sync,
|
||||
T::TlsConnect: std::marker::Send + std::marker::Sync,
|
||||
<T::TlsConnect as tokio_postgres::tls::TlsConnect<tokio_postgres::Socket>>::Future: std::marker::Send,
|
||||
{
|
||||
let manager_config = deadpool_postgres::ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Verified };
|
||||
let manager = deadpool_postgres::Manager::from_config(pg_config, tls, manager_config);
|
||||
let built = deadpool_postgres::Pool::builder(manager)
|
||||
.max_size(settings.max_connections as usize)
|
||||
.wait_timeout(std::option::Option::Some(settings.wait_timeout))
|
||||
.create_timeout(std::option::Option::Some(settings.create_timeout))
|
||||
.recycle_timeout(std::option::Option::Some(settings.recycle_timeout))
|
||||
.runtime(deadpool_postgres::Runtime::Tokio1)
|
||||
.build();
|
||||
return match built {
|
||||
std::result::Result::Ok(pool) => std::result::Result::Ok(pool),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_build")),
|
||||
};
|
||||
}
|
||||
|
||||
fn build_verified_tls() -> std::result::Result<tokio_postgres_rustls::MakeRustlsConnect, crate::PostgresBackendError> {
|
||||
let native = rustls_native_certs::load_native_certs();
|
||||
let native_error_count = native.errors.len();
|
||||
if native.certs.is_empty() {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, "no system TLS roots available for PostgreSQL verify_full");
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
|
||||
}
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
let (added, ignored) = roots.add_parsable_certificates(native.certs);
|
||||
if added == 0 {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, ignored, "system TLS roots could not be admitted for PostgreSQL verify_full");
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
|
||||
}
|
||||
if native_error_count > 0 || ignored > 0 {
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, added, ignored, native_error_count, "loaded PostgreSQL system TLS roots with partial diagnostics");
|
||||
}
|
||||
let provider = std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
||||
let builder_result = rustls::ClientConfig::builder_with_provider(provider).with_safe_default_protocol_versions();
|
||||
let builder = match builder_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "protocol_versions"));
|
||||
},
|
||||
};
|
||||
let client_config = builder.with_root_certificates(roots).with_no_client_auth();
|
||||
return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config));
|
||||
}
|
||||
|
||||
fn map_pool_error(error: deadpool_postgres::PoolError) -> crate::PostgresBackendError {
|
||||
return match error {
|
||||
deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new(
|
||||
crate::PostgresBackendErrorKind::PoolTimeout,
|
||||
match timeout_type {
|
||||
deadpool_postgres::TimeoutType::Wait => "pool_wait",
|
||||
deadpool_postgres::TimeoutType::Create => "pool_create",
|
||||
deadpool_postgres::TimeoutType::Recycle => "pool_recycle",
|
||||
},
|
||||
),
|
||||
deadpool_postgres::PoolError::Backend(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "physical_connect"),
|
||||
deadpool_postgres::PoolError::Closed => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_closed"),
|
||||
deadpool_postgres::PoolError::NoRuntimeSpecified => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_runtime"),
|
||||
deadpool_postgres::PoolError::PostCreateHook(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_post_create"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/runtime.rs"]
|
||||
mod tests;
|
||||
@@ -1,36 +1,60 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Dependency-boundary canaries for the PostgreSQL Store backend scaffold.
|
||||
//! Dependency and ownership canaries for the physical PostgreSQL Store backend.
|
||||
|
||||
#[test]
|
||||
fn pre_002_backend_depends_on_store_api_without_reverse_facade_edge() {
|
||||
fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
||||
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] {
|
||||
for required in ["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "tokio-postgres", "tokio-postgres-rustls"] {
|
||||
assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}");
|
||||
}
|
||||
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_backend_does_not_advance_connection_pool_tls_or_migrations() {
|
||||
let manifest = include_str!("../Cargo.toml");
|
||||
for forbidden in ["tokio", "tokio-postgres", "deadpool-postgres", "tokio-postgres-rustls", "rustls", "sha2"] {
|
||||
assert!(!manifest.contains(forbidden), "premature PostgreSQL runtime dependency detected: {forbidden}");
|
||||
}
|
||||
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(crate_root.contains("mod constants;"));
|
||||
assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;"));
|
||||
assert!(crate_root.contains("const _: &str = TRACING_TARGET;"));
|
||||
for forbidden in ["pub mod ", "pub struct", "pub enum", "pub trait", "tokio_postgres", "deadpool_postgres", "mod migration", "SELECT ", "CREATE TABLE"] {
|
||||
assert!(!crate_root.contains(forbidden), "premature PostgreSQL backend surface detected: {forbidden}");
|
||||
assert!(crate_root.contains("mod error;"));
|
||||
assert!(crate_root.contains("mod runtime;"));
|
||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||
for forbidden in [
|
||||
"pub mod ",
|
||||
"ksp_store_lib",
|
||||
"ksp_config_lib",
|
||||
"tokio_postgres::Client",
|
||||
"tokio_postgres::Row",
|
||||
"tokio_postgres::Statement",
|
||||
"deadpool_postgres::Pool;",
|
||||
] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}");
|
||||
}
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for forbidden in [
|
||||
"std::env",
|
||||
"dotenv",
|
||||
"KSP_",
|
||||
"KSPB_",
|
||||
"PGHOST",
|
||||
"PGPORT",
|
||||
"PGUSER",
|
||||
"PGPASSWORD",
|
||||
".pgpass",
|
||||
"CREATE TABLE",
|
||||
"INSERT INTO",
|
||||
"UPDATE ",
|
||||
"DELETE FROM",
|
||||
"SELECT ",
|
||||
"mod migration",
|
||||
] {
|
||||
assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}");
|
||||
}
|
||||
let constants = include_str!("../src/constants.rs");
|
||||
assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-postgres-lib\";"));
|
||||
return;
|
||||
}
|
||||
|
||||
44
crates/ksp-store-postgres-lib/tests/public_api.rs
Normal file
44
crates/ksp-store-postgres-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Narrow physical bridge canaries consumed by `ksp-store-lib` without exposing driver or pool types.
|
||||
|
||||
#[test]
|
||||
fn pre_005_backend_bridge_is_constructible_without_io() {
|
||||
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid backend bridge network rejected: {error:?}"),
|
||||
};
|
||||
let settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||
network,
|
||||
"postgresql://operator:secret@localhost/ksp",
|
||||
8,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||
);
|
||||
assert_eq!(settings.network().as_str(), "devnet");
|
||||
assert_eq!(settings.tls_mode().code(), "verify_full");
|
||||
let _open = ksp_store_postgres_lib::PostgresBackend::open;
|
||||
let _close = ksp_store_postgres_lib::PostgresBackend::close;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_005_backend_error_projection_is_safe_and_static() {
|
||||
let kinds = [
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
|
||||
];
|
||||
assert_eq!(kinds.len(), 5);
|
||||
return;
|
||||
}
|
||||
99
crates/ksp-store-postgres-lib/unit_tests/runtime.rs
Normal file
99
crates/ksp-store-postgres-lib/unit_tests/runtime.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs
|
||||
// version: 1
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid backend test network rejected: {error:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn settings(connection_uri: &str, tls_mode: crate::PostgresBackendTlsMode) -> crate::PostgresBackendSettings {
|
||||
return crate::PostgresBackendSettings::new(
|
||||
network(),
|
||||
connection_uri,
|
||||
8,
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(10),
|
||||
std::time::Duration::from_secs(5),
|
||||
tls_mode,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physical_settings_debug_redacts_connection_uri() {
|
||||
let secret = "postgresql://secret-user:secret-password@localhost/ksp";
|
||||
let value = settings(secret, crate::PostgresBackendTlsMode::VerifyFull);
|
||||
let rendered = format!("{value:?}");
|
||||
assert!(!rendered.contains("secret-user"));
|
||||
assert!(!rendered.contains("secret-password"));
|
||||
assert!(rendered.contains("<redacted>"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_or_oversized_uri_is_rejected_without_retaining_input() {
|
||||
let malformed = "not-a-postgresql-uri-secret-canary";
|
||||
let malformed_result = super::normalized_config(&settings(malformed, crate::PostgresBackendTlsMode::Disabled));
|
||||
let malformed_error = malformed_result.err();
|
||||
assert_eq!(malformed_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
assert!(!format!("{malformed_error:?}").contains("secret-canary"));
|
||||
let oversized = "x".repeat(super::MAX_CONNECTION_URI_BYTES + 1);
|
||||
let oversized_result = super::normalized_config(&settings(oversized.as_str(), crate::PostgresBackendTlsMode::Disabled));
|
||||
assert_eq!(oversized_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_disabled_policy_overrides_uri_tls_and_connection_controls() {
|
||||
let value = settings(
|
||||
"postgresql://operator:secret@localhost/ksp?sslmode=require&application_name=hostile&connect_timeout=1&sslnegotiation=direct",
|
||||
crate::PostgresBackendTlsMode::Disabled,
|
||||
);
|
||||
let config = match super::normalized_config(&value) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => panic!("valid disabled config rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Disable);
|
||||
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
|
||||
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
|
||||
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(std::time::Duration::from_secs(10)));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_verify_full_policy_forces_tls_and_rejects_hostaddr_only_identity() {
|
||||
let value = settings(
|
||||
"postgresql://operator:secret@localhost/ksp?sslmode=disable&application_name=hostile&connect_timeout=1",
|
||||
crate::PostgresBackendTlsMode::VerifyFull,
|
||||
);
|
||||
let config = match super::normalized_config(&value) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => panic!("valid verify_full config rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Require);
|
||||
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
|
||||
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
|
||||
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(std::time::Duration::from_secs(10)));
|
||||
let hostaddr_only = settings("hostaddr=127.0.0.1 user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
|
||||
let rejected = super::normalized_config(&hostaddr_only);
|
||||
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let unix_socket = settings("host=/var/run/postgresql user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
|
||||
let unix_rejected = super::normalized_config(&unix_socket);
|
||||
assert_eq!(unix_rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libpq_server_options_are_rejected_in_foundation_runtime() {
|
||||
let value = settings("host=localhost user=operator dbname=ksp options='-c statement_timeout=0'", crate::PostgresBackendTlsMode::Disabled);
|
||||
let rejected = super::normalized_config(&value);
|
||||
let error = rejected.err();
|
||||
assert_eq!(error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||
assert_eq!(error.map(|value| return value.phase()), std::option::Option::Some("server_options"));
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user