#![cfg(all(feature = "testkit", feature = "cli"))]
use meterstore::cli::{Cli, Command, Format};
fn config_file(url: &str, warehouse: &std::path::Path) -> tempfile::NamedTempFile {
use std::io::Write;
let mut file = tempfile::NamedTempFile::new().expect("temp file");
write!(
file,
r#"
[hot]
url = "{url}"
max_connections = 4
ddl_lock_timeout = "2s"
[cold]
catalog = "sql"
uri = "{url}"
warehouse = "file://{}"
namespace = "metering"
file_target_bytes = 8388608
metadata_pool_max_connections = 2
[[tables]]
name = "readings_versions"
[tables.archival]
settlement_lag = "1d"
archival_step = "1d"
"#,
warehouse.display(),
)
.expect("write");
file
}
fn cli(config: &std::path::Path, command: Command) -> Cli {
Cli {
config: config.to_path_buf(),
format: Format::Json,
log: "off".to_string(),
command,
}
}
#[tokio::test]
async fn the_commands_drive_a_real_deployment() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let file = config_file(&url, warehouse.path());
let path = file.path();
cli(path, Command::Check).run().await.expect("check");
cli(path, Command::Create).run().await.expect("create");
cli(path, Command::Status)
.run()
.await
.expect("a freshly created table is healthy");
cli(
path,
Command::Archive {
table: None,
max_windows: 4,
},
)
.run()
.await
.expect("archive");
cli(path, Command::Snapshots { table: None })
.run()
.await
.expect("the archival committed a snapshot to list");
cli(
path,
Command::Query {
sql: "SELECT count(*) AS n FROM readings".to_string(),
historical: false,
operational: false,
},
)
.run()
.await
.expect("query");
cli(
path,
Command::Explain {
sql: "SELECT malo_id, SUM(value) FROM readings GROUP BY 1".to_string(),
},
)
.run()
.await
.expect("explain");
cli(
path,
Command::Completeness {
table: None,
from: None,
to: None,
month: Some("2026-06".to_string()),
sparte: "STROM".to_string(),
seen_since: Some("30d".to_string()),
malo: None,
melo: None,
obis: None,
gaps_only: false,
},
)
.run()
.await
.expect("completeness over a Bilanzierungsmonat");
cli(
path,
Command::Completeness {
table: Some("readings".to_string()),
from: Some("2026-06-01T00:00:00Z".to_string()),
to: Some("2026-07-01T00:00:00Z".to_string()),
month: None,
sparte: "STROM".to_string(),
seen_since: None,
malo: Some("12345678905".to_string()),
melo: Some("DE0001234567890123456789012345678".to_string()),
obis: Some("1-0:1.29.0".to_string()),
gaps_only: true,
},
)
.run()
.await
.expect("completeness over an explicit range");
}
#[tokio::test]
async fn a_statement_that_will_not_plan_is_not_reported_as_retryable() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let file = config_file(&url, warehouse.path());
cli(file.path(), Command::Create)
.run()
.await
.expect("create");
let err = cli(
file.path(),
Command::Query {
sql: "SELECT * FROM no_such_relation".to_string(),
historical: false,
operational: false,
},
)
.run()
.await
.expect_err("there is no such relation");
assert!(!err.is_retryable(), "{err:?}");
}
#[tokio::test]
async fn a_table_flag_takes_either_of_a_tables_two_names() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let file = config_file(&url, warehouse.path());
let path = file.path();
cli(path, Command::Create).run().await.expect("create");
for name in ["readings_versions", "readings"] {
cli(
path,
Command::Archive {
table: Some(name.to_string()),
max_windows: 1,
},
)
.run()
.await
.unwrap_or_else(|e| panic!("--table {name} should select the table: {e}"));
}
let err = cli(
path,
Command::Archive {
table: Some("esa_typ2".to_string()),
max_windows: 1,
},
)
.run()
.await
.expect_err("no such table");
assert!(err.to_string().contains("readings_versions"), "{err}");
}
#[tokio::test]
async fn purge_needs_the_table_named_twice() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let file = config_file(&url, warehouse.path());
cli(file.path(), Command::Create)
.run()
.await
.expect("create");
let err = cli(
file.path(),
Command::Purge {
table: "readings_versions".to_string(),
confirm: "readings".to_string(),
},
)
.run()
.await
.expect_err("the confirmation does not match, and there is no recovery path");
assert!(err.to_string().contains("readings_versions"), "{err}");
cli(file.path(), Command::Status)
.run()
.await
.expect("status");
}
fn config_with_subjects(url: &str, warehouse: &std::path::Path) -> tempfile::NamedTempFile {
use std::io::Write;
let mut file = tempfile::NamedTempFile::new().expect("temp file");
write!(
file,
r#"
[hot]
url = "{url}"
max_connections = 4
[cold]
catalog = "sql"
uri = "{url}"
warehouse = "file://{}"
namespace = "metering"
file_target_bytes = 8388608
metadata_pool_max_connections = 2
[[tables]]
name = "readings_versions"
subject_column = "subject_ref"
[tables.archival]
settlement_lag = "1d"
archival_step = "1d"
"#,
warehouse.display(),
)
.expect("write");
file
}
#[tokio::test]
async fn the_erasure_trail_is_readable_from_the_shell() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let file = config_with_subjects(&url, warehouse.path());
let path = file.path();
cli(path, Command::Create).run().await.expect("create");
cli(
path,
Command::Erasures {
limit: 50,
since: None,
until: None,
trigger: None,
},
)
.run()
.await
.expect("an empty trail reports as empty");
let deployment = meterstore::Settings::from_path(path)
.expect("settings")
.connect()
.await
.expect("connect");
let catalog = deployment.catalog().await.expect("catalog");
let store = catalog.table("readings_versions").expect("table");
let subject = store
.register_subject(
"tenant-a:12345678905",
time::macros::datetime!(2026-07-20 00:00 UTC),
metering::interval::Sparte::Strom,
)
.await
.expect("register");
store
.erase_subject(
&subject,
"DSAR-2026-0042",
"privacy-team",
time::macros::datetime!(2026-08-31 09:00 UTC),
)
.await
.expect("erase");
cli(
path,
Command::Erasures {
limit: 50,
since: None,
until: None,
trigger: None,
},
)
.run()
.await
.expect("the trail now holds a row");
cli(
path,
Command::Erasures {
limit: 50,
since: Some("2026-08-01T00:00:00Z".to_string()),
until: Some("2026-09-01T00:00:00Z".to_string()),
trigger: Some("request".to_string()),
},
)
.run()
.await
.expect("a period and a duty");
let err = cli(
path,
Command::Erasures {
limit: 0,
since: None,
until: None,
trigger: None,
},
)
.run()
.await
.expect_err("a limit is a row count");
assert!(err.to_string().contains("--limit"), "{err}");
for (since, until, trigger) in [
(Some("last tuesday"), None, None),
(None, None, Some("sweep")),
(
Some("2026-10-01T00:00:00Z"),
Some("2026-07-01T00:00:00Z"),
None,
),
] {
let err = cli(
path,
Command::Erasures {
limit: 50,
since: since.map(str::to_string),
until: until.map(str::to_string),
trigger: trigger.map(str::to_string),
},
)
.run()
.await
.expect_err("{since:?} {until:?} {trigger:?} should be refused");
assert!(!err.to_string().is_empty());
}
}
#[tokio::test]
async fn asking_for_erasures_where_no_subject_is_declared_says_so() {
let url = meterstore::testkit::postgres::fresh_database()
.await
.expect("postgres");
let warehouse = tempfile::tempdir().expect("temp warehouse");
let file = config_file(&url, warehouse.path());
cli(file.path(), Command::Create)
.run()
.await
.expect("create");
let err = cli(
file.path(),
Command::Erasures {
limit: 10,
since: None,
until: None,
trigger: None,
},
)
.run()
.await
.expect_err("no subject column is declared");
assert!(err.to_string().contains("subject_column"), "{err}");
}