use rstest::rstest;
use super::{
super::fixture::{CannedResponse, serve_once, serve_sequence, temp_root, unreachable_url},
path_source,
sample_manifest,
};
use crate::{
error::BootstrapErrorKind,
extensions::{ManifestSource, Sha256Hex, running_version, version::parse_pg_config_version},
};
#[test]
fn load_from_path_verifies_digest_and_reports_missing() {
let (_temp, dir) = temp_root().expect("fixture");
let text = sample_manifest().expect("fixture");
let path = dir.join("manifest.json");
std::fs::write(&path, &text).expect("write manifest");
let good = ManifestSource::Path {
path: path.clone(),
sha256: Some(Sha256Hex::of_bytes(text.as_bytes())),
};
crate::extensions::manifest::load(&good).expect("digest matches");
let bad = ManifestSource::Path {
path: path.clone(),
sha256: Some(Sha256Hex::of_bytes(b"other")),
};
let err = crate::extensions::manifest::load(&bad).expect_err("digest mismatch");
assert_eq!(
err.kind(),
BootstrapErrorKind::ExtensionManifestDigestMismatch
);
let missing = path_source(dir.join("absent.json"));
let missing_err = crate::extensions::manifest::load(&missing).expect_err("missing file");
assert_eq!(
missing_err.kind(),
BootstrapErrorKind::ExtensionManifestUnavailable
);
}
fn url_source(url: &str, digest: Sha256Hex) -> ManifestSource {
ManifestSource::Url {
url: url.to_owned(),
sha256: digest,
}
}
#[test]
fn load_from_a_loopback_url_verifies_the_digest() {
let text = sample_manifest().expect("fixture");
let url = serve_once(text.clone().into_bytes()).expect("server");
let manifest =
crate::extensions::manifest::load(&url_source(&url, Sha256Hex::of_bytes(text.as_bytes())))
.expect("a pinned loopback manifest loads");
assert_eq!(
manifest.release, "v1.0.0",
"the served release must survive"
);
assert_eq!(
manifest.extensions.len(),
1,
"the served manifest declares one extension"
);
}
#[test]
fn a_loopback_manifest_that_misses_its_pin_is_refused() {
let text = sample_manifest().expect("fixture");
let url = serve_once(text.into_bytes()).expect("server");
let err = crate::extensions::manifest::load(&url_source(
&url,
Sha256Hex::of_bytes(b"a manifest that was never served"),
))
.expect_err("the pin must be enforced over HTTP");
assert_eq!(
err.kind(),
BootstrapErrorKind::ExtensionManifestDigestMismatch
);
}
#[rstest]
#[case::refused(None, BootstrapErrorKind::ExtensionManifestUnavailable)]
#[case::not_found(Some(b"" as &[u8]), BootstrapErrorKind::ExtensionManifestUnavailable)]
#[case::not_a_manifest(Some(b"{}" as &[u8]), BootstrapErrorKind::ExtensionManifestInvalid)]
fn a_url_manifest_failure_keeps_its_kind(
#[case] served: Option<&[u8]>,
#[case] expected: BootstrapErrorKind,
) {
let url = match served {
None => unreachable_url().expect("port"),
Some(b"") => serve_sequence(vec![CannedResponse::status("404 Not Found")]).expect("server"),
Some(body) => serve_once(body.to_vec()).expect("server"),
};
let digest = Sha256Hex::of_bytes(served.unwrap_or_default());
let err = crate::extensions::manifest::load(&url_source(&url, digest))
.expect_err("the load must fail");
assert_eq!(err.kind(), expected, "{err}");
}
#[rstest]
#[case::plain("PostgreSQL 17.11\n", Some((17, 11)))]
#[case::debian("PostgreSQL 16.4 (Debian 16.4-1)", Some((16, 4)))]
#[case::devel("PostgreSQL 19devel", Some((19, 0)))]
#[case::garbage("nothing here", None)]
#[case::empty("", None)]
fn pg_config_version_parses(#[case] text: &str, #[case] expected: Option<(u64, u64)>) {
let parsed = parse_pg_config_version(text).map(|v| (v.major, v.minor));
assert_eq!(parsed, expected);
}
#[test]
fn running_version_from_dir_name_or_fails_closed() {
let (_temp, root) = temp_root().expect("fixture");
let versioned = root.join("17.11.0");
std::fs::create_dir_all(&versioned).expect("mkdir");
let version = running_version(&versioned).expect("dir name parses");
assert_eq!((version.major, version.minor), (17, 11));
let unnamed = root.join("install");
std::fs::create_dir_all(&unnamed).expect("mkdir");
let err = running_version(&unnamed).expect_err("no pg_config either");
assert_eq!(err.kind(), BootstrapErrorKind::ExtensionUnavailable);
}
#[cfg(unix)]
#[test]
fn running_version_falls_back_to_pg_config() {
use std::os::unix::fs::PermissionsExt;
let (_temp, root) = temp_root().expect("tempdir");
let install = root.join("install");
std::fs::create_dir_all(install.join("bin")).expect("mkdir");
let script = install.join("bin/pg_config");
std::fs::write(
&script,
"#!/bin/sh\necho 'PostgreSQL 16.4 (Debian 16.4-1)'\n",
)
.expect("write");
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).expect("chmod");
let version = running_version(&install).expect("pg_config answers");
assert_eq!((version.major, version.minor), (16, 4));
std::fs::write(&script, "#!/bin/sh\necho 'broken' >&2\nexit 3\n").expect("write");
let err = running_version(&install).expect_err("non-zero exit fails closed");
assert_eq!(err.kind(), BootstrapErrorKind::ExtensionUnavailable);
assert!(err.to_string().contains("broken"), "{err}");
}