#![cfg(all(unix, feature = "remote-s3"))]
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const FLOCI_IMAGE: &str = "floci/floci";
const FLOCI_PORT_INSIDE: &str = "4566";
struct FlociContainer {
name: String,
port: u16,
}
impl FlociContainer {
fn start() -> Option<Self> {
let name = format!(
"cartog-floci-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let status = Command::new("docker")
.args([
"run",
"--rm",
"-d",
"--name",
&name,
"-p",
&format!("0:{FLOCI_PORT_INSIDE}"),
FLOCI_IMAGE,
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok()?;
if !status.success() {
return None;
}
macro_rules! abort {
() => {{
let _ = Command::new("docker")
.args(["kill", &name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
return None;
}};
}
let out = match Command::new("docker")
.args(["port", &name, FLOCI_PORT_INSIDE])
.output()
{
Ok(o) => o,
Err(_) => abort!(),
};
let stdout = String::from_utf8_lossy(&out.stdout);
let port: u16 = match stdout
.lines()
.find_map(|l| l.rsplit(':').next()?.trim().parse().ok())
{
Some(p) => p,
None => abort!(),
};
let endpoint = format!("http://localhost:{port}");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let probe = Command::new("aws")
.args(["--endpoint-url", &endpoint, "s3", "ls"])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
if matches!(probe, Ok(s) if s.success()) {
break;
}
if Instant::now() > deadline {
abort!();
}
std::thread::sleep(Duration::from_millis(100));
}
Some(Self { name, port })
}
fn endpoint(&self) -> String {
format!("http://localhost:{}", self.port)
}
}
impl Drop for FlociContainer {
fn drop(&mut self) {
let _ = Command::new("docker")
.args(["kill", &self.name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
fn which(name: &str) -> Option<PathBuf> {
Command::new("which")
.arg(name)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| PathBuf::from(s.trim()))
}
fn skip_unless_deps_present() -> bool {
for tool in ["docker", "aws", "git"] {
if which(tool).is_none() {
eprintln!("SKIP: `{tool}` not on PATH");
return true;
}
}
false
}
fn create_bucket(endpoint: &str, bucket: &str) {
let st = Command::new("aws")
.args([
"--endpoint-url",
endpoint,
"s3",
"mb",
&format!("s3://{bucket}"),
])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("run aws s3 mb");
assert!(st.success(), "aws s3 mb failed");
}
fn build_minimal_index(repo_dir: &Path, db_path: &Path) {
std::fs::create_dir_all(repo_dir).unwrap();
std::fs::write(repo_dir.join("hello.py"), "def greet():\n return 'hi'\n").unwrap();
let git = |args: &[&str]| {
Command::new("git")
.args(args)
.current_dir(repo_dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
};
assert!(
matches!(git(&["init", "-q"]), Ok(s) if s.success()),
"git init failed in build_minimal_index"
);
assert!(
matches!(git(&["add", "-A"]), Ok(s) if s.success()),
"git add failed in build_minimal_index"
);
let committed = git(&[
"-c",
"user.email=ci@cartog.test",
"-c",
"user.name=cartog ci",
"commit",
"-q",
"-m",
"init",
]);
assert!(
matches!(committed, Ok(s) if s.success()),
"git commit failed in build_minimal_index; provenance tests need a HEAD"
);
let st = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args([
"--db",
&db_path.to_string_lossy(),
"index",
"--no-lsp",
&repo_dir.to_string_lossy(),
])
.env_remove("CARTOG_DB")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("spawn cartog index");
assert!(st.success(), "cartog index failed");
assert!(
db_path.exists(),
"DB was not created at {}",
db_path.display()
);
}
#[test]
fn push_pull_roundtrip_against_floci() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-roundtrip");
let work = tempfile::TempDir::new().unwrap();
let repo = work.path().join("repo");
let src_db = work.path().join("src.sqlite");
let dst_db = work.path().join("dst.sqlite");
build_minimal_index(&repo, &src_db);
let src_bytes = std::fs::read(&src_db).unwrap();
let env = &[
("AWS_ACCESS_KEY_ID", "test"),
("AWS_SECRET_ACCESS_KEY", "test"),
("AWS_DEFAULT_REGION", "us-east-1"),
];
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-roundtrip/index.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &src_db.to_string_lossy(), "push"])
.current_dir(&cfg_dir)
.envs(env.iter().copied())
.output()
.unwrap();
assert!(
out.status.success(),
"push failed:\nstdout={}\nstderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let head = Command::new("aws")
.args([
"--endpoint-url",
&endpoint,
"s3api",
"head-object",
"--bucket",
"cartog-roundtrip",
"--key",
"index.sqlite",
])
.envs(env.iter().copied())
.output()
.unwrap();
assert!(head.status.success(), "head-object failed: {:?}", head);
let head_json = String::from_utf8_lossy(&head.stdout);
for header in ["sha256", "schema-version", "cartog-version", "git-commit"] {
assert!(
head_json.contains(&format!("\"{header}\":")),
"push did not set x-amz-meta-{header}; full head-object output:\n{head_json}"
);
}
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.envs(env.iter().copied())
.output()
.unwrap();
assert!(
out.status.success(),
"pull failed:\nstdout={}\nstderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let dst_bytes = std::fs::read(&dst_db).unwrap();
assert_eq!(src_bytes, dst_bytes, "round-tripped DB differs from source");
}
#[test]
fn pull_refuses_on_checksum_mismatch() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-corrupt");
let work = tempfile::TempDir::new().unwrap();
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-corrupt/index.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let payload_path = work.path().join("payload");
std::fs::write(&payload_path, b"not really a sqlite file").unwrap();
let st = Command::new("aws")
.args([
"--endpoint-url",
&endpoint,
"s3",
"cp",
&payload_path.to_string_lossy(),
"s3://cartog-corrupt/index.sqlite",
])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.status()
.unwrap();
assert!(st.success());
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.output()
.unwrap();
assert!(!out.status.success(), "pull must fail on missing checksum");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("sha256") || stderr.contains("checksum"),
"expected checksum error, got: {stderr}"
);
assert!(
!dst_db.exists(),
"destination DB must be absent after failed pull"
);
let partial = work.path().join("dst.sqlite.partial");
assert!(!partial.exists(), "partial file must be cleaned up");
}
#[test]
fn anonymous_pull_on_missing_object_fails_cleanly() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-anon");
let work = tempfile::TempDir::new().unwrap();
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-anon/missing-object.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args([
"--db",
&dst_db.to_string_lossy(),
"pull",
"--no-sign-request",
])
.current_dir(&cfg_dir)
.env_remove("AWS_ACCESS_KEY_ID")
.env_remove("AWS_SECRET_ACCESS_KEY")
.output()
.unwrap();
assert!(
!out.status.success(),
"anonymous pull of a non-existent object must fail:\nstdout={}\nstderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dst_db.exists(),
"destination DB must not exist after a failed pull"
);
let partial = work.path().join("dst.sqlite.partial");
assert!(
!partial.exists(),
"partial file must be cleaned up after a failed pull"
);
}
#[test]
fn pull_refuses_non_cartog_sqlite_with_valid_sha() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-foreign-sqlite");
let work = tempfile::TempDir::new().unwrap();
let foreign_db = work.path().join("foreign.sqlite");
{
let conn = rusqlite::Connection::open(&foreign_db).unwrap();
conn.execute_batch(
"PRAGMA journal_mode = DELETE; \
PRAGMA synchronous = FULL; \
CREATE TABLE notes(content TEXT); \
INSERT INTO notes VALUES ('hello'); \
PRAGMA wal_checkpoint(TRUNCATE);",
)
.unwrap();
conn.close().expect("close foreign.sqlite cleanly");
}
std::fs::File::open(&foreign_db)
.unwrap()
.sync_all()
.expect("fsync foreign.sqlite");
let bytes = std::fs::read(&foreign_db).unwrap();
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(&bytes);
let sha = h
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let claimed_v = cartog::db::CURRENT_SCHEMA_VERSION;
let st = Command::new("aws")
.args([
"--endpoint-url",
&endpoint,
"s3",
"cp",
&foreign_db.to_string_lossy(),
"s3://cartog-foreign-sqlite/index.sqlite",
"--metadata",
&format!("sha256={sha},schema-version={claimed_v}"),
])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.status()
.unwrap();
assert!(st.success(), "aws s3 cp failed");
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-foreign-sqlite/index.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.output()
.unwrap();
assert!(
!out.status.success(),
"pull of a non-cartog SQLite file must fail even with a matching sha256"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("not a cartog database"),
"expected 'not a cartog database' refusal, got: {stderr}"
);
assert!(!dst_db.exists(), "destination must not be created");
let partial = work.path().join("dst.sqlite.partial");
assert!(!partial.exists(), "partial file must be cleaned up");
}
#[test]
fn push_refuses_when_config_was_rejected() {
let work = tempfile::TempDir::new().unwrap();
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
"[remote]\nurl = \"s3://b/k\"\naccess_key = \"AKIA...\"\n",
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let db_path = work.path().join("db.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &db_path.to_string_lossy(), "push"])
.current_dir(&cfg_dir)
.env_remove("CARTOG_DB")
.output()
.unwrap();
assert!(
!out.status.success(),
"push must fail when config was rejected"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("configuration file") && stderr.contains("was rejected"),
"expected explicit config-rejected error, got: {stderr}"
);
assert!(
stderr.contains("credential"),
"expected the underlying security reason to be surfaced, got: {stderr}"
);
}
#[test]
fn pull_without_explicit_path_style_uses_path_style_against_floci() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-default-pathstyle");
let work = tempfile::TempDir::new().unwrap();
let repo = work.path().join("repo");
let src_db = work.path().join("src.sqlite");
build_minimal_index(&repo, &src_db);
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-default-pathstyle/index.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let env = [
("AWS_ACCESS_KEY_ID", "test"),
("AWS_SECRET_ACCESS_KEY", "test"),
("AWS_DEFAULT_REGION", "us-east-1"),
];
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &src_db.to_string_lossy(), "push"])
.current_dir(&cfg_dir)
.envs(env.iter().copied())
.output()
.unwrap();
assert!(
out.status.success(),
"push without explicit path_style failed:\nstdout={}\nstderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.envs(env.iter().copied())
.output()
.unwrap();
assert!(
out.status.success(),
"pull without explicit path_style failed:\nstdout={}\nstderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
std::fs::read(&src_db).unwrap(),
std::fs::read(&dst_db).unwrap()
);
}
fn upload_with_schema_version(
floci_endpoint: &str,
bucket: &str,
key: &str,
file_v: u32,
header_v: u32,
) -> tempfile::TempDir {
let work = tempfile::TempDir::new().unwrap();
let repo = work.path().join("repo");
let src_db = work.path().join("src.sqlite");
build_minimal_index(&repo, &src_db);
{
let conn = rusqlite::Connection::open(&src_db).unwrap();
conn.execute(
"UPDATE metadata SET value = ?1 WHERE key = 'schema_version'",
[&file_v.to_string()],
)
.unwrap();
}
use sha2::{Digest, Sha256};
let bytes = std::fs::read(&src_db).unwrap();
let mut h = Sha256::new();
h.update(&bytes);
let sha = h
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let st = Command::new("aws")
.args([
"--endpoint-url",
floci_endpoint,
"s3",
"cp",
&src_db.to_string_lossy(),
&format!("s3://{bucket}/{key}"),
"--metadata",
&format!("sha256={sha},schema-version={header_v},cartog-version=test"),
])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.status()
.unwrap();
assert!(st.success(), "aws s3 cp failed");
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://{bucket}/{key}"
region = "us-east-1"
endpoint = "{floci_endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
work
}
#[test]
fn pull_refuses_future_schema_version() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-future");
let future_v = cartog::db::CURRENT_SCHEMA_VERSION + 100;
let work = upload_with_schema_version(
&endpoint,
"cartog-future",
"index.sqlite",
future_v,
future_v,
);
let cfg_dir = work.path().join("cfg");
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.output()
.unwrap();
assert!(
!out.status.success(),
"pull of a future-version DB must fail"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Upgrade cartog") || stderr.contains("supports up to"),
"expected future-version refusal, got: {stderr}"
);
assert!(!dst_db.exists(), "destination must not be created");
}
#[test]
fn pull_refuses_header_vs_file_mismatch() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-version-skew");
let work = upload_with_schema_version(
&endpoint,
"cartog-version-skew",
"index.sqlite",
cartog::db::CURRENT_SCHEMA_VERSION,
cartog::db::CURRENT_SCHEMA_VERSION.saturating_sub(1).max(1),
);
let cfg_dir = work.path().join("cfg");
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.output()
.unwrap();
assert!(
!out.status.success(),
"pull with header/file schema mismatch must fail"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("schema-version mismatch"),
"expected mismatch refusal, got: {stderr}"
);
assert!(!dst_db.exists(), "destination must not be created");
}
#[test]
fn pull_refuses_git_commit_header_vs_file_mismatch() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-commit-skew");
let work = tempfile::TempDir::new().unwrap();
let repo = work.path().join("repo");
let src_db = work.path().join("src.sqlite");
build_minimal_index(&repo, &src_db);
use sha2::{Digest, Sha256};
let bytes = std::fs::read(&src_db).unwrap();
let mut h = Sha256::new();
h.update(&bytes);
let sha = h
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let claimed_v = cartog::db::CURRENT_SCHEMA_VERSION;
let st = Command::new("aws")
.args([
"--endpoint-url",
&endpoint,
"s3",
"cp",
&src_db.to_string_lossy(),
"s3://cartog-commit-skew/index.sqlite",
"--metadata",
&format!("sha256={sha},schema-version={claimed_v},git-commit=deadbeefdeadbeef"),
])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.status()
.unwrap();
assert!(st.success(), "aws s3 cp failed");
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-commit-skew/index.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.output()
.unwrap();
assert!(
!out.status.success(),
"pull with git-commit header/file mismatch must fail"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("git-commit mismatch"),
"expected git-commit mismatch refusal, got: {stderr}"
);
assert!(!dst_db.exists(), "destination must not be created");
let partial = work.path().join("dst.sqlite.partial");
assert!(!partial.exists(), "partial file must be cleaned up");
}
#[test]
fn pull_reports_malformed_schema_version_header() {
if skip_unless_deps_present() {
return;
}
let floci = match FlociContainer::start() {
Some(f) => f,
None => {
eprintln!("SKIP: could not start floci container");
return;
}
};
let endpoint = floci.endpoint();
create_bucket(&endpoint, "cartog-bad-schema-header");
let work = tempfile::TempDir::new().unwrap();
let repo = work.path().join("repo");
let src_db = work.path().join("src.sqlite");
build_minimal_index(&repo, &src_db);
use sha2::{Digest, Sha256};
let bytes = std::fs::read(&src_db).unwrap();
let mut h = Sha256::new();
h.update(&bytes);
let sha = h
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let st = Command::new("aws")
.args([
"--endpoint-url",
&endpoint,
"s3",
"cp",
&src_db.to_string_lossy(),
"s3://cartog-bad-schema-header/index.sqlite",
"--metadata",
&format!("sha256={sha},schema-version=notanumber"),
])
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.status()
.unwrap();
assert!(st.success(), "aws s3 cp failed");
let cfg_dir = work.path().join("cfg");
std::fs::create_dir_all(&cfg_dir).unwrap();
std::fs::write(
cfg_dir.join(".cartog.toml"),
format!(
r#"[remote]
url = "s3://cartog-bad-schema-header/index.sqlite"
region = "us-east-1"
endpoint = "{endpoint}"
path_style = true
"#
),
)
.unwrap();
let _ = Command::new("git")
.args(["init", "-q"])
.current_dir(&cfg_dir)
.status();
let dst_db = work.path().join("dst.sqlite");
let out = Command::new(env!("CARGO_BIN_EXE_cartog"))
.args(["--db", &dst_db.to_string_lossy(), "pull"])
.current_dir(&cfg_dir)
.env("AWS_ACCESS_KEY_ID", "test")
.env("AWS_SECRET_ACCESS_KEY", "test")
.env("AWS_DEFAULT_REGION", "us-east-1")
.output()
.unwrap();
assert!(
!out.status.success(),
"pull with malformed schema-version header must fail"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("malformed"),
"expected 'malformed' refusal, got: {stderr}"
);
assert!(
!stderr.contains("has no"),
"must not report a present-but-bad header as missing: {stderr}"
);
assert!(!dst_db.exists(), "destination must not be created");
}