Files
MercuryToolbox/crates/sqliteshape/tests/sqliteshape_cli.rs
T

184 lines
6.3 KiB
Rust

//! Integration tests for the `sqliteshape` command.
use assert_cmd::Command;
use predicates::prelude::*;
use rusqlite::Connection;
use serde_json::Value;
use std::fmt::Write as _;
use tempfile::tempdir;
fn cargo_command() -> Command {
Command::cargo_bin("sqliteshape").expect("binary")
}
#[test]
fn summarizes_sqlite_as_json() {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("sample.db");
let connection = Connection::open(&path).expect("db");
connection
.execute_batch(
"CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT);
INSERT INTO users(name, city) VALUES ('Ada', 'London'), ('Bob', NULL);",
)
.expect("schema");
drop(connection);
let mut command = cargo_command();
command
.arg("--json")
.arg(&path)
.assert()
.success()
.stdout(predicate::str::contains("\"tables\""))
.stdout(predicate::str::contains("\"name\":\"users\""))
.stdout(predicate::str::contains("\"declared_type\":\"TEXT\""));
}
#[test]
fn help_includes_sqlite_examples() {
let mut command = cargo_command();
command
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"sqliteshape [OPTIONS] diff <BEFORE> <AFTER>",
))
.stdout(predicate::str::contains("--table"))
.stdout(predicate::str::contains("ConvertFrom-Json"))
.stdout(predicate::str::contains("sqliteshape"));
}
#[test]
fn diff_subcommand_reports_table_and_column_changes_as_json() {
let temp = tempdir().expect("tempdir");
let before_path = temp.path().join("before.db");
let after_path = temp.path().join("after.db");
let before = Connection::open(&before_path).expect("before db");
before
.execute_batch(
"CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL);
INSERT INTO users(name) VALUES ('Ada');",
)
.expect("before schema");
drop(before);
let after = Connection::open(&after_path).expect("after db");
after
.execute_batch(
"CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT);
CREATE TABLE events(id INTEGER PRIMARY KEY, kind TEXT);
INSERT INTO users(name, city) VALUES ('Ada', 'London'), ('Bob', NULL);
CREATE INDEX idx_users_city ON users(city);",
)
.expect("after schema");
drop(after);
let mut command = cargo_command();
command
.arg("--json")
.arg("diff")
.arg("--include-indexes")
.arg(&before_path)
.arg(&after_path)
.assert()
.success()
.stdout(predicate::str::contains("\"path_before\""))
.stdout(predicate::str::contains("\"path_after\""))
.stdout(predicate::str::contains("\"added_tables\""))
.stdout(predicate::str::contains("\"name\":\"events\""))
.stdout(predicate::str::contains("\"changed_tables\""))
.stdout(predicate::str::contains("\"name\":\"users\""))
.stdout(predicate::str::contains("\"added_columns\""))
.stdout(predicate::str::contains("\"city\""))
.stdout(predicate::str::contains("\"added_indexes\""))
.stdout(predicate::str::contains("\"idx_users_city\""));
}
#[test]
fn wide_table_with_expression_index_summarizes_without_index_column_decode_failure() {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("wide_expression.db");
let connection = Connection::open(&path).expect("db");
let mut schema =
String::from("CREATE TABLE documents(id INTEGER PRIMARY KEY, title TEXT NOT NULL");
for index in 0..96 {
write!(schema, ", c_{index:03} TEXT").expect("schema write");
}
schema.push_str(");\n");
schema.push_str(
"CREATE INDEX idx_documents_lower_title ON documents(lower(title)) WHERE title IS NOT NULL;",
);
connection.execute_batch(&schema).expect("schema");
drop(connection);
let output = Command::cargo_bin("sqliteshape")
.expect("binary")
.args(["--json", "--include-indexes"])
.arg(&path)
.output()
.expect("run sqliteshape");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"sqliteshape failed with {}:\n{stdout}\n{stderr}",
output.status
);
let report: Value = serde_json::from_str(&stdout).expect("json report");
let table = &report["tables"][0];
assert_eq!(table["name"], "documents");
assert_eq!(table["columns"].as_array().expect("columns").len(), 98);
assert_eq!(
table["indexes"][0]["columns"].as_array().expect("columns")[0],
"<expression>"
);
}
#[test]
fn malicious_catalog_invalid_table_sql_exits_with_bounded_diagnostic() {
let temp = tempdir().expect("tempdir");
let path = temp.path().join("malicious_catalog_invalid_sql.db");
let connection = Connection::open(&path).expect("db");
connection
.execute_batch(
"CREATE TABLE users(id INTEGER PRIMARY KEY);
PRAGMA writable_schema = ON;
UPDATE sqlite_schema
SET sql = 'CREATE TABLE users('
WHERE type = 'table' AND name = 'users';
PRAGMA writable_schema = OFF;",
)
.expect("malicious catalog");
drop(connection);
let output = Command::cargo_bin("sqliteshape")
.expect("binary")
.args(["--json", "--include-indexes"])
.arg(&path)
.output()
.expect("run sqliteshape");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"sqliteshape unexpectedly accepted malicious catalog:\n{stdout}\n{stderr}"
);
assert!(stdout.trim().is_empty(), "unexpected stdout: {stdout}");
assert!(
stderr.contains("malformed")
|| stderr.contains("failed to list tables")
|| stderr.contains("failed to inspect columns"),
"unexpected malicious catalog diagnostic:\n{stderr}"
);
assert!(
!stderr.contains("panicked") && !stderr.contains("backtrace"),
"diagnostic should be fail-closed, not a panic:\n{stderr}"
);
assert!(stderr.len() < 512, "unbounded diagnostic:\n{stderr}");
}