584 lines
25 KiB
Rust
584 lines
25 KiB
Rust
// file: ks-store/src/postgres/store.rs
|
|
// version: 9
|
|
|
|
//! PostgreSQL store implementation kept behind the backend-agnostic `Store` facade.
|
|
|
|
/// PostgreSQL store connection options interpreted only inside `ks-store`.
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub(crate) struct PostgresStoreOptions {
|
|
database_url: std::string::String,
|
|
max_connections: u32,
|
|
connect_timeout_ms: u64,
|
|
auto_initialize_schema: bool,
|
|
option_count: u32,
|
|
}
|
|
|
|
impl crate::PostgresStoreOptions {
|
|
/// Creates validated PostgreSQL store options for crate-internal tests and adapters.
|
|
#[cfg(test)]
|
|
pub(crate) fn new(
|
|
database_url: impl std::convert::Into<std::string::String>,
|
|
max_connections: u32,
|
|
connect_timeout_ms: u64,
|
|
auto_initialize_schema: bool,
|
|
) -> ks_core::Result<Self> {
|
|
return Self::new_with_option_count(
|
|
database_url,
|
|
max_connections,
|
|
connect_timeout_ms,
|
|
auto_initialize_schema,
|
|
4,
|
|
);
|
|
}
|
|
|
|
/// Interprets the selected opaque backend options supplied to `Store::open`.
|
|
pub(crate) fn from_backend_options(options: &serde_json::Value) -> ks_core::Result<Self> {
|
|
let object = match options.as_object() {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_invalid",
|
|
"postgres backend options must be a JSON object",
|
|
));
|
|
},
|
|
};
|
|
let database_url = match object.get("url").and_then(serde_json::Value::as_str) {
|
|
std::option::Option::Some(value) => value.to_string(),
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_incomplete",
|
|
"postgres backend requires a non-empty connection URL",
|
|
));
|
|
},
|
|
};
|
|
let max_connections_u64 =
|
|
match object.get("max_connections").and_then(serde_json::Value::as_u64) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_incomplete",
|
|
"postgres backend requires max_connections greater than zero",
|
|
));
|
|
},
|
|
};
|
|
let max_connections = match u32::try_from(max_connections_u64) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_invalid",
|
|
"postgres max_connections does not fit into u32",
|
|
));
|
|
},
|
|
};
|
|
let connect_timeout_ms =
|
|
match object.get("connect_timeout_ms").and_then(serde_json::Value::as_u64) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_incomplete",
|
|
"postgres backend requires connect_timeout_ms greater than zero",
|
|
));
|
|
},
|
|
};
|
|
let auto_initialize_schema =
|
|
match object.get("auto_initialize_schema").and_then(serde_json::Value::as_bool) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_incomplete",
|
|
"postgres backend requires auto_initialize_schema",
|
|
));
|
|
},
|
|
};
|
|
let option_count = match u32::try_from(object.len()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => u32::MAX,
|
|
};
|
|
return Self::new_with_option_count(
|
|
database_url,
|
|
max_connections,
|
|
connect_timeout_ms,
|
|
auto_initialize_schema,
|
|
option_count,
|
|
);
|
|
}
|
|
|
|
fn new_with_option_count(
|
|
database_url: impl std::convert::Into<std::string::String>,
|
|
max_connections: u32,
|
|
connect_timeout_ms: u64,
|
|
auto_initialize_schema: bool,
|
|
option_count: u32,
|
|
) -> ks_core::Result<Self> {
|
|
let database_url_value = database_url.into();
|
|
if database_url_value.trim().is_empty() {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_incomplete",
|
|
"postgres backend requires a non-empty connection URL",
|
|
));
|
|
}
|
|
if max_connections == 0 {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_invalid",
|
|
"postgres max_connections must be greater than zero",
|
|
));
|
|
}
|
|
if connect_timeout_ms == 0 {
|
|
return std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_options_invalid",
|
|
"postgres connect_timeout_ms must be greater than zero",
|
|
));
|
|
}
|
|
return std::result::Result::Ok(Self {
|
|
database_url: database_url_value,
|
|
max_connections,
|
|
connect_timeout_ms,
|
|
auto_initialize_schema,
|
|
option_count,
|
|
});
|
|
}
|
|
|
|
/// Returns whether automatic schema initialization is enabled.
|
|
pub(crate) fn auto_initialize_schema(&self) -> bool {
|
|
return self.auto_initialize_schema;
|
|
}
|
|
|
|
/// Returns a backend-neutral safe configuration summary.
|
|
pub(crate) fn configuration_summary(&self) -> crate::StoreConfigurationSummary {
|
|
return crate::StoreConfigurationSummary {
|
|
enabled: true,
|
|
backend_code: "postgres".to_string(),
|
|
connection_configured: !self.database_url.trim().is_empty(),
|
|
auto_initialize_schema: self.auto_initialize_schema,
|
|
backend_option_count: self.option_count,
|
|
};
|
|
}
|
|
|
|
/// Returns a DSN masked for diagnostics.
|
|
pub(crate) fn masked_connection_descriptor(&self) -> std::string::String {
|
|
return mask_postgres_dsn(self.database_url.as_str());
|
|
}
|
|
}
|
|
|
|
/// PostgreSQL store handle kept private to `ks-store`.
|
|
#[derive(Clone)]
|
|
pub(crate) struct PostgresStore {
|
|
options: crate::PostgresStoreOptions,
|
|
pool: sqlx::PgPool,
|
|
}
|
|
|
|
impl crate::PostgresStore {
|
|
/// Connects to PostgreSQL from validated backend options.
|
|
pub(crate) async fn connect(options: crate::PostgresStoreOptions) -> ks_core::Result<Self> {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", "open PostgreSQL store connection");
|
|
let pool_options = sqlx::postgres::PgPoolOptions::new()
|
|
.max_connections(options.max_connections)
|
|
.acquire_timeout(std::time::Duration::from_millis(options.connect_timeout_ms));
|
|
let connect_result = pool_options.connect(options.database_url.as_str()).await;
|
|
return match connect_result {
|
|
std::result::Result::Ok(pool) => {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", connected = true, "PostgreSQL store connection opened");
|
|
std::result::Result::Ok(Self { options, pool })
|
|
},
|
|
std::result::Result::Err(_error) => {
|
|
tracing::error!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", connected = false, "PostgreSQL store connection failed");
|
|
std::result::Result::Err(crate::storage_contract_error(
|
|
"store_backend_connection_failed",
|
|
"postgres connection failed",
|
|
))
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Returns the underlying PostgreSQL pool to backend-private repositories.
|
|
pub(crate) fn pool(&self) -> &sqlx::PgPool {
|
|
return &self.pool;
|
|
}
|
|
|
|
/// Applies each idempotent store schema once per invocation in dependency order.
|
|
pub(crate) async fn initialize_store_schema(&self) -> ks_core::Result<()> {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "schema_initialize", "initialize PostgreSQL store schema");
|
|
let raw_result = crate::apply_raw_store_schema(&self.pool).await;
|
|
if let std::result::Result::Err(error) = raw_result {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let core_result = crate::apply_core_store_schema(&self.pool).await;
|
|
if let std::result::Result::Err(error) = core_result {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let result = crate::apply_decode_store_schema(&self.pool).await;
|
|
if result.is_ok() {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "schema_initialize", initialized = true, "PostgreSQL store schema initialized");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Applies the idempotent minimal raw Solana store schema.
|
|
#[cfg(test)]
|
|
pub(crate) async fn initialize_raw_store_schema(&self) -> ks_core::Result<()> {
|
|
return crate::apply_raw_store_schema(&self.pool).await;
|
|
}
|
|
|
|
/// Applies the idempotent minimal Core Solana store schema.
|
|
#[cfg(test)]
|
|
pub(crate) async fn initialize_core_store_schema(&self) -> ks_core::Result<()> {
|
|
let raw_result = self.initialize_raw_store_schema().await;
|
|
if let std::result::Result::Err(error) = raw_result {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return crate::apply_core_store_schema(&self.pool).await;
|
|
}
|
|
|
|
/// Reads a UI-safe backend descriptor.
|
|
pub(crate) async fn backend_descriptor(
|
|
&self,
|
|
) -> ks_core::Result<crate::StoreBackendDescriptor> {
|
|
let schema_result = crate::load_current_schema(&self.pool).await;
|
|
return match schema_result {
|
|
std::result::Result::Ok(schema) => crate::StoreBackendDescriptor::new(
|
|
"postgres",
|
|
"PostgreSQL",
|
|
std::option::Option::Some(self.options.masked_connection_descriptor()),
|
|
std::option::Option::Some(schema),
|
|
),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
/// Reads a PostgreSQL health snapshot without exposing backend error details.
|
|
pub(crate) async fn health_snapshot(&self) -> ks_core::Result<crate::StoreHealthSnapshot> {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "health_check", "run PostgreSQL store health check");
|
|
let health_result = crate::run_health_check(&self.pool).await;
|
|
return match health_result {
|
|
std::result::Result::Ok(()) => crate::StoreHealthSnapshot::new(
|
|
"postgres",
|
|
crate::StoreHealthStatus::Healthy,
|
|
std::option::Option::Some(std::string::String::from(
|
|
"backend health check succeeded",
|
|
)),
|
|
),
|
|
std::result::Result::Err(_error) => crate::StoreHealthSnapshot::new(
|
|
"postgres",
|
|
crate::StoreHealthStatus::Unhealthy,
|
|
std::option::Option::Some(std::string::String::from("backend health check failed")),
|
|
),
|
|
};
|
|
}
|
|
|
|
/// Reads a non-destructive migration snapshot.
|
|
pub(crate) async fn migration_snapshot(
|
|
&self,
|
|
) -> ks_core::Result<crate::StoreMigrationSnapshot> {
|
|
let migration_table_result = crate::load_migration_table_name(&self.pool).await;
|
|
return match migration_table_result {
|
|
std::result::Result::Ok(std::option::Option::None) => {
|
|
std::result::Result::Ok(crate::StoreMigrationSnapshot::new(
|
|
crate::StoreMigrationStatus::NotInitialized,
|
|
std::option::Option::None,
|
|
std::vec::Vec::new(),
|
|
std::option::Option::Some(std::string::String::from(
|
|
"no migration history table detected; crate-managed schema initialization is active",
|
|
)),
|
|
))
|
|
},
|
|
std::result::Result::Ok(std::option::Option::Some(_table_name)) => {
|
|
self.migration_snapshot_from_existing_table().await
|
|
},
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
/// Reads a complete backend-neutral diagnostic snapshot.
|
|
pub(crate) async fn backend_diagnostics(
|
|
&self,
|
|
) -> ks_core::Result<crate::StoreBackendDiagnostics> {
|
|
let descriptor = match self.backend_descriptor().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let health = match self.health_snapshot().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let migrations = match self.migration_snapshot().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let backend_version = match crate::load_server_version(&self.pool).await {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(_error) => std::option::Option::None,
|
|
};
|
|
return std::result::Result::Ok(crate::StoreBackendDiagnostics {
|
|
descriptor,
|
|
health,
|
|
migrations,
|
|
backend_version,
|
|
});
|
|
}
|
|
|
|
/// Lists bounded raw transaction candidates enriched with Core and ledger diagnostics.
|
|
pub(crate) async fn replay_transaction_candidates(
|
|
&self,
|
|
filter: &crate::ReplayTransactionFilter,
|
|
) -> ks_core::Result<std::vec::Vec<crate::ReplayTransactionCandidate>> {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_transaction_candidates", "query PostgreSQL replay transaction candidates");
|
|
return crate::list_replay_transaction_candidates(&self.pool, filter).await;
|
|
}
|
|
|
|
/// Lists bounded program summaries across top-level, inner and reliably linked logs.
|
|
pub(crate) async fn replay_program_summaries(
|
|
&self,
|
|
filter: &crate::ReplayProgramFilter,
|
|
) -> ks_core::Result<std::vec::Vec<crate::ReplayProgramSummary>> {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_program_summaries", "query PostgreSQL replay program summaries");
|
|
return crate::list_replay_program_summaries(&self.pool, filter).await;
|
|
}
|
|
|
|
/// Lists bounded mint, owner or account-key summaries from Core facts.
|
|
pub(crate) async fn replay_entity_summaries(
|
|
&self,
|
|
filter: &crate::ReplayEntityFilter,
|
|
) -> ks_core::Result<std::vec::Vec<crate::ReplayEntitySummary>> {
|
|
tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_entity_summaries", "query PostgreSQL replay entity summaries");
|
|
return crate::list_replay_entity_summaries(&self.pool, filter).await;
|
|
}
|
|
|
|
/// Reads diagnostics for raw store resources without changing the schema.
|
|
pub(crate) async fn raw_resource_diagnostics(
|
|
&self,
|
|
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
|
|
let specs = crate::raw_store_table_diagnostic_specs();
|
|
return self.resource_diagnostics(&specs).await;
|
|
}
|
|
|
|
/// Reads diagnostics for Core and processing store resources without changing the schema.
|
|
pub(crate) async fn core_resource_diagnostics(
|
|
&self,
|
|
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
|
|
let specs = crate::core_store_table_diagnostic_specs();
|
|
return self.resource_diagnostics(&specs).await;
|
|
}
|
|
|
|
/// Reads diagnostics for decode and materialization resources without changing the schema.
|
|
pub(crate) async fn decode_resource_diagnostics(
|
|
&self,
|
|
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
|
|
let specs = crate::decode_store_table_diagnostic_specs();
|
|
return self.resource_diagnostics(&specs).await;
|
|
}
|
|
|
|
/// Reads diagnostics for every known logical store resource.
|
|
pub(crate) async fn known_resource_diagnostics(
|
|
&self,
|
|
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
|
|
let mut diagnostics = std::vec::Vec::new();
|
|
let raw_resources = match self.raw_resource_diagnostics().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
diagnostics.extend(raw_resources);
|
|
let core_resources = match self.core_resource_diagnostics().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
diagnostics.extend(core_resources);
|
|
let decode_resources = match self.decode_resource_diagnostics().await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
diagnostics.extend(decode_resources);
|
|
return std::result::Result::Ok(diagnostics);
|
|
}
|
|
|
|
async fn resource_diagnostics(
|
|
&self,
|
|
specs: &[crate::PostgresTableDiagnosticSpec],
|
|
) -> ks_core::Result<std::vec::Vec<crate::StoreResourceDiagnostics>> {
|
|
let mut diagnostics = std::vec::Vec::new();
|
|
for spec in specs {
|
|
let available = match crate::table_exists(&self.pool, spec.table_name).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let statistics = if available {
|
|
let statistics_result =
|
|
crate::load_table_statistics(&self.pool, spec.table_name).await;
|
|
match statistics_result {
|
|
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
} else {
|
|
std::option::Option::None
|
|
};
|
|
diagnostics.push(crate::StoreResourceDiagnostics {
|
|
resource_code: spec.resource_code.to_string(),
|
|
model_code: spec.model_code.to_string(),
|
|
role: spec.role.to_string(),
|
|
available,
|
|
statistics,
|
|
});
|
|
}
|
|
return std::result::Result::Ok(diagnostics);
|
|
}
|
|
|
|
async fn migration_snapshot_from_existing_table(
|
|
&self,
|
|
) -> ks_core::Result<crate::StoreMigrationSnapshot> {
|
|
let version_result = crate::load_latest_migration_version(&self.pool).await;
|
|
return match version_result {
|
|
std::result::Result::Ok(current_version) => {
|
|
std::result::Result::Ok(crate::StoreMigrationSnapshot::new(
|
|
crate::StoreMigrationStatus::Current,
|
|
current_version,
|
|
std::vec::Vec::new(),
|
|
std::option::Option::Some(std::string::String::from(
|
|
"migration history table detected",
|
|
)),
|
|
))
|
|
},
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Returns a PostgreSQL connection descriptor masked for logs and diagnostics.
|
|
fn mask_postgres_dsn(dsn: &str) -> std::string::String {
|
|
let trimmed_dsn = dsn.trim();
|
|
if trimmed_dsn.is_empty() {
|
|
return std::string::String::from("");
|
|
}
|
|
let queryless = strip_query(trimmed_dsn);
|
|
return match queryless.split_once("://") {
|
|
std::option::Option::Some((scheme, remainder)) => {
|
|
mask_scheme_remainder(scheme, remainder, trimmed_dsn.contains('?'))
|
|
},
|
|
std::option::Option::None => mask_plain_dsn(queryless.as_str()),
|
|
};
|
|
}
|
|
|
|
fn strip_query(dsn: &str) -> std::string::String {
|
|
return match dsn.split_once('?') {
|
|
std::option::Option::Some((prefix, _query)) => std::string::String::from(prefix),
|
|
std::option::Option::None => std::string::String::from(dsn),
|
|
};
|
|
}
|
|
|
|
fn mask_scheme_remainder(scheme: &str, remainder: &str, had_query: bool) -> std::string::String {
|
|
let suffix = query_suffix(had_query);
|
|
return match remainder.rsplit_once('@') {
|
|
std::option::Option::Some((_userinfo, host_path)) => {
|
|
format!("{scheme}://***:***@{host_path}{suffix}")
|
|
},
|
|
std::option::Option::None => format!("{scheme}://{remainder}{suffix}"),
|
|
};
|
|
}
|
|
|
|
fn mask_plain_dsn(dsn: &str) -> std::string::String {
|
|
if dsn.contains("password=") {
|
|
return std::string::String::from("<postgres-dsn-redacted>");
|
|
}
|
|
return std::string::String::from(dsn);
|
|
}
|
|
|
|
fn query_suffix(had_query: bool) -> std::string::String {
|
|
if had_query {
|
|
return std::string::String::from("?<redacted>");
|
|
}
|
|
return std::string::String::from("");
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn backend_options_reject_empty_database_url() {
|
|
let result = crate::PostgresStoreOptions::new(" ", 1, 1000, false);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn backend_options_reject_zero_max_connections() {
|
|
let result = crate::PostgresStoreOptions::new("postgres://localhost/db", 0, 1000, false);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn incomplete_backend_options_do_not_echo_secret_values() {
|
|
let options = serde_json::json!({
|
|
"url": "postgres://operator:STORE-SECRET-CANARY@localhost/db",
|
|
"connect_timeout_ms": 5000,
|
|
"auto_initialize_schema": true
|
|
});
|
|
let error = match crate::PostgresStoreOptions::from_backend_options(&options) {
|
|
std::result::Result::Ok(_) => panic!("incomplete backend options must be rejected"),
|
|
std::result::Result::Err(error) => error,
|
|
};
|
|
assert!(!error.to_string().contains("STORE-SECRET-CANARY"));
|
|
assert!(!error.to_string().contains("postgres://"));
|
|
}
|
|
|
|
#[test]
|
|
fn opaque_backend_options_are_sanitized_in_summary() {
|
|
let options = serde_json::json!({
|
|
"url": "postgres://operator:STORE-SECRET-CANARY@localhost/db",
|
|
"max_connections": 4,
|
|
"connect_timeout_ms": 5000,
|
|
"auto_initialize_schema": true
|
|
});
|
|
let parsed = crate::PostgresStoreOptions::from_backend_options(&options);
|
|
let value = match parsed {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("backend options must parse: {error}"),
|
|
};
|
|
let serialized = match serde_json::to_string(&value.configuration_summary()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("summary must serialize: {error}"),
|
|
};
|
|
assert!(!serialized.contains("STORE-SECRET-CANARY"));
|
|
assert!(!serialized.contains("postgres://"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn optional_postgres_healthcheck_from_env() {
|
|
let database_url = match std::env::var("KS_SECRET_POSTGRES_TEST_URL") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_error) => return,
|
|
};
|
|
let _postgres_guard = crate::postgres_test_guard().await;
|
|
let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false);
|
|
let options = match options_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("unexpected options error: {error}"),
|
|
};
|
|
let store_result = crate::PostgresStore::connect(options).await;
|
|
let store = match store_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("unexpected connect error: {error}"),
|
|
};
|
|
let health_result = store.health_snapshot().await;
|
|
let health = match health_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("unexpected health error: {error}"),
|
|
};
|
|
assert_eq!(health.status, crate::StoreHealthStatus::Healthy);
|
|
}
|
|
|
|
#[test]
|
|
fn mask_postgres_dsn_masks_userinfo() {
|
|
let masked = super::mask_postgres_dsn("postgres://user:secret@localhost:5432/db");
|
|
assert_eq!(masked, "postgres://***:***@localhost:5432/db");
|
|
}
|
|
|
|
#[test]
|
|
fn mask_postgres_dsn_masks_query_string() {
|
|
let masked =
|
|
super::mask_postgres_dsn("postgres://localhost/db?sslmode=require&password=secret");
|
|
assert_eq!(masked, "postgres://localhost/db?<redacted>");
|
|
}
|
|
|
|
#[test]
|
|
fn mask_postgres_dsn_masks_plain_password_dsn() {
|
|
let masked = super::mask_postgres_dsn("host=localhost password=secret dbname=khadhroony");
|
|
assert_eq!(masked, "<postgres-dsn-redacted>");
|
|
}
|
|
}
|