Define configuration and CLI contracts (#2)
This commit is contained in:
343
crates/storage/tests/config_contract.rs
Normal file
343
crates/storage/tests/config_contract.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::{error::Error, ffi::OsStr, fs, path::Path};
|
||||
|
||||
use ironstorage::config::{ConfigError, ConfigLoader, EditorSource};
|
||||
use tempfile::TempDir;
|
||||
|
||||
type TestResult = Result<(), Box<dyn Error>>;
|
||||
|
||||
struct ConfigurationFixture {
|
||||
temporary: TempDir,
|
||||
}
|
||||
|
||||
impl ConfigurationFixture {
|
||||
fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let temporary = tempfile::tempdir()?;
|
||||
fs::create_dir_all(temporary.path().join("cwd/config/keys"))?;
|
||||
fs::create_dir_all(temporary.path().join("native"))?;
|
||||
Ok(Self { temporary })
|
||||
}
|
||||
|
||||
fn loader(&self) -> ConfigLoader {
|
||||
ConfigLoader::new(
|
||||
self.temporary.path().join("cwd"),
|
||||
self.temporary.path().join("native"),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_explicit(&self, contents: &str) -> Result<(), Box<dyn Error>> {
|
||||
fs::write(self.explicit_path(), contents)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn explicit_path(&self) -> std::path::PathBuf {
|
||||
self.temporary.path().join("cwd/config/config.toml")
|
||||
}
|
||||
|
||||
fn valid_contents(&self) -> &'static str {
|
||||
r#"
|
||||
vault = "../vault"
|
||||
default_key = "0123456789ABCDEF0123456789ABCDEF01234567"
|
||||
key_material = "keys"
|
||||
editor = ["code", "--wait"]
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "https://git.example.test/alice/store.git"
|
||||
server_id = "personal-git"
|
||||
application_id = "ironstorage-cli"
|
||||
"#
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_relative_configuration_resolves_deterministically() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(fixture.valid_contents())?;
|
||||
let config = fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))?;
|
||||
|
||||
assert_eq!(config.source(), fixture.explicit_path());
|
||||
assert_eq!(config.vault(), fixture.temporary.path().join("cwd/vault"));
|
||||
assert_eq!(
|
||||
config.key_material(),
|
||||
fs::canonicalize(fixture.temporary.path().join("cwd/config/keys"))?
|
||||
);
|
||||
assert_eq!(
|
||||
config.default_key().as_str(),
|
||||
"0123456789ABCDEF0123456789ABCDEF01234567"
|
||||
);
|
||||
assert_eq!(config.git_remotes().len(), 1);
|
||||
let remote = &config.git_remotes()[0];
|
||||
assert_eq!(remote.name().as_str(), "origin");
|
||||
assert_eq!(
|
||||
remote.url().as_str(),
|
||||
"https://git.example.test/alice/store.git"
|
||||
);
|
||||
assert_eq!(remote.server_id().as_str(), "personal-git");
|
||||
assert_eq!(remote.application_id().as_str(), "ironstorage-cli");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_default_path_is_used_without_an_explicit_path() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
let default = fixture.loader().default_path();
|
||||
fs::create_dir_all(default.parent().expect("default parent"))?;
|
||||
fs::create_dir_all(default.parent().expect("default parent").join("keys"))?;
|
||||
fs::write(&default, fixture.valid_contents())?;
|
||||
|
||||
let config = fixture.loader().load(None)?;
|
||||
assert_eq!(config.source(), default);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_precedence_and_argument_splitting_are_storage_owned() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(fixture.valid_contents())?;
|
||||
let config = fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))?;
|
||||
let editor = config.resolve_editor_from(
|
||||
Some(OsStr::new("visual --ignored")),
|
||||
Some(OsStr::new("editor --ignored")),
|
||||
)?;
|
||||
assert_eq!(editor.source(), EditorSource::Configuration);
|
||||
assert_eq!(editor.command().program(), "code");
|
||||
assert_eq!(editor.command().arguments(), ["--wait"]);
|
||||
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice@example.test"
|
||||
key_material = "keys"
|
||||
"#,
|
||||
)?;
|
||||
let config = fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))?;
|
||||
let visual = config.resolve_editor_from(
|
||||
Some(OsStr::new("code --wait 'two words'")),
|
||||
Some(OsStr::new("nano")),
|
||||
)?;
|
||||
assert_eq!(visual.source(), EditorSource::VisualEnvironment);
|
||||
assert_eq!(visual.command().program(), "code");
|
||||
assert_eq!(visual.command().arguments(), ["--wait", "two words"]);
|
||||
|
||||
let editor = config.resolve_editor_from(None, Some(OsStr::new("nano -w")))?;
|
||||
assert_eq!(editor.source(), EditorSource::EditorEnvironment);
|
||||
assert_eq!(editor.command().program(), "nano");
|
||||
assert_eq!(editor.command().arguments(), ["-w"]);
|
||||
|
||||
let fallback = config.resolve_editor_from(None, None)?;
|
||||
assert_eq!(fallback.source(), EditorSource::Fallback);
|
||||
assert_eq!(fallback.command().program(), "vim");
|
||||
assert!(fallback.command().arguments().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_unknown_and_insecure_configuration_are_redacted() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit("vault = [\"unterminated\"\npassword = \"do-not-repeat\"")?;
|
||||
let error = fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("malformed TOML");
|
||||
assert!(matches!(error, ConfigError::Malformed { .. }));
|
||||
assert!(!error.to_string().contains("do-not-repeat"));
|
||||
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
colour = "blue"
|
||||
"#,
|
||||
)?;
|
||||
let error = fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("unknown field");
|
||||
assert_eq!(
|
||||
error,
|
||||
ConfigError::UnknownField {
|
||||
field: "colour".to_owned()
|
||||
}
|
||||
);
|
||||
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
token = "do-not-repeat"
|
||||
"#,
|
||||
)?;
|
||||
let error = fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("secret field");
|
||||
assert_eq!(
|
||||
error,
|
||||
ConfigError::InsecureField {
|
||||
field: "token".to_owned()
|
||||
}
|
||||
);
|
||||
assert!(!error.to_string().contains("do-not-repeat"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_and_invalid_required_fields_are_typed() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit("default_key = \"alice\"\nkey_material = \"keys\"\n")?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("missing vault"),
|
||||
ConfigError::MissingField { field: "vault" }
|
||||
);
|
||||
|
||||
fixture.write_explicit("vault = \"vault\"\ndefault_key = \" \"\nkey_material = \"keys\"\n")?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("empty identity"),
|
||||
ConfigError::InvalidField {
|
||||
field: "default_key"
|
||||
}
|
||||
);
|
||||
|
||||
fixture.write_explicit(
|
||||
"vault = \"vault\"\ndefault_key = \"alice\"\nkey_material = \"missing\"\n",
|
||||
)?;
|
||||
assert!(matches!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("missing keys"),
|
||||
ConfigError::KeyMaterialNotFound { .. }
|
||||
));
|
||||
|
||||
fs::write(
|
||||
fixture.temporary.path().join("cwd/config/not-a-vault"),
|
||||
b"file",
|
||||
)?;
|
||||
fixture.write_explicit(
|
||||
"vault = \"not-a-vault\"\ndefault_key = \"alice\"\nkey_material = \"keys\"\n",
|
||||
)?;
|
||||
assert!(matches!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("vault file"),
|
||||
ConfigError::VaultIsNotDirectory { .. }
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_configuration_rejects_non_https_and_embedded_credentials() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
for url in [
|
||||
"ssh://git@example.test/store.git",
|
||||
"git://example.test/store.git",
|
||||
"file:///tmp/store.git",
|
||||
"../store.git",
|
||||
"https://user:password@example.test/store.git",
|
||||
"https://example.test/store.git?token=do-not-store",
|
||||
"https://example.test/store.git#fragment",
|
||||
] {
|
||||
fixture.write_explicit(&format!(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "{url}"
|
||||
server_id = "server"
|
||||
application_id = "application"
|
||||
"#
|
||||
))?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("forbidden remote"),
|
||||
ConfigError::InvalidRemoteUrl {
|
||||
name: "origin".to_owned()
|
||||
},
|
||||
"URL should be rejected without entering transport: {url}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_remote_names_and_credential_references_are_rejected() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "https://one.example.test/store.git"
|
||||
server_id = "one"
|
||||
application_id = "app"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "https://two.example.test/store.git"
|
||||
server_id = "two"
|
||||
application_id = "app"
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("duplicate name"),
|
||||
ConfigError::DuplicateRemote {
|
||||
name: "origin".to_owned()
|
||||
}
|
||||
);
|
||||
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "one"
|
||||
url = "https://one.example.test/store.git"
|
||||
server_id = "server"
|
||||
application_id = "app"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "two"
|
||||
url = "https://two.example.test/store.git"
|
||||
server_id = "server"
|
||||
application_id = "app"
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(Path::new("config/config.toml")))
|
||||
.expect_err("duplicate credential reference"),
|
||||
ConfigError::DuplicateCredentialReference
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user