forked from Crockan/MercuryToolbox
chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
//! 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}");
|
||||
}
|
||||
Reference in New Issue
Block a user