forked from Crockan/MercuryToolbox
184 lines
6.3 KiB
Rust
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}");
|
|
}
|