diff options
Diffstat (limited to 'vendor/gix-credentials/tests')
17 files changed, 731 insertions, 0 deletions
diff --git a/vendor/gix-credentials/tests/credentials.rs b/vendor/gix-credentials/tests/credentials.rs new file mode 100644 index 000000000..f94dfd05e --- /dev/null +++ b/vendor/gix-credentials/tests/credentials.rs @@ -0,0 +1,5 @@ +pub use gix_testtools::Result; + +mod helper; +mod program; +mod protocol; diff --git a/vendor/gix-credentials/tests/fixtures/all-but-credentials.sh b/vendor/gix-credentials/tests/fixtures/all-but-credentials.sh new file mode 100644 index 000000000..0610a71b0 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/all-but-credentials.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo protocol=ftp +echo host=example.com:8080 +echo path=/path/to/git/ + diff --git a/vendor/gix-credentials/tests/fixtures/custom-helper.sh b/vendor/gix-credentials/tests/fixtures/custom-helper.sh new file mode 100755 index 000000000..55d6b2f94 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/custom-helper.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu + +test "$1" = get && \ +echo username=user-script && \ +echo password=pass-script diff --git a/vendor/gix-credentials/tests/fixtures/fail.sh b/vendor/gix-credentials/tests/fixtures/fail.sh new file mode 100644 index 000000000..84b6391bc --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/fail.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exit 42 diff --git a/vendor/gix-credentials/tests/fixtures/last-pass.sh b/vendor/gix-credentials/tests/fixtures/last-pass.sh new file mode 100644 index 000000000..2f03f8986 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/last-pass.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu + +echo username=user +echo password=pass +echo quit=1 diff --git a/vendor/gix-credentials/tests/fixtures/password.sh b/vendor/gix-credentials/tests/fixtures/password.sh new file mode 100644 index 000000000..f75c4bc02 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/password.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo password=pass diff --git a/vendor/gix-credentials/tests/fixtures/reflect.sh b/vendor/gix-credentials/tests/fixtures/reflect.sh new file mode 100644 index 000000000..e4079b793 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/reflect.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cat diff --git a/vendor/gix-credentials/tests/fixtures/url.sh b/vendor/gix-credentials/tests/fixtures/url.sh new file mode 100644 index 000000000..1eb585859 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/url.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo protocol=ftp +echo host=github.com +echo path=byron/gitoxide +echo url=http://example.com:8080/path/to/git/ + diff --git a/vendor/gix-credentials/tests/fixtures/username.sh b/vendor/gix-credentials/tests/fixtures/username.sh new file mode 100644 index 000000000..f2bab6c28 --- /dev/null +++ b/vendor/gix-credentials/tests/fixtures/username.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo username=user diff --git a/vendor/gix-credentials/tests/helper/cascade.rs b/vendor/gix-credentials/tests/helper/cascade.rs new file mode 100644 index 000000000..f97a64dcb --- /dev/null +++ b/vendor/gix-credentials/tests/helper/cascade.rs @@ -0,0 +1,174 @@ +mod invoke { + use std::convert::TryInto; + + use bstr::{ByteSlice, ByteVec}; + use gix_credentials::{ + helper::{Action, Cascade}, + program, protocol, + protocol::Context, + Program, + }; + use gix_sec::identity::Account; + use gix_testtools::fixture_path; + + #[test] + fn credentials_are_filled_in_one_by_one_and_stop_when_complete() { + let actual = invoke_cascade(["username", "password", "custom-helper"], action_get()) + .unwrap() + .expect("credentials"); + assert_eq!(actual.identity, identity("user", "pass")); + } + + #[test] + fn usernames_in_urls_are_kept_if_the_helper_does_not_overwrite_it() { + let actual = invoke_cascade( + ["password", "custom-helper"], + Action::get_for_url("ssh://git@host.org/path"), + ) + .unwrap() + .expect("credentials"); + assert_eq!(actual.identity, identity("git", "pass")); + } + + #[test] + fn partial_credentials_can_be_overwritten_by_complete_ones() { + let actual = invoke_cascade(["username", "custom-helper"], action_get()) + .unwrap() + .expect("credentials"); + assert_eq!(actual.identity, identity("user-script", "pass-script")); + } + + #[test] + fn failing_helpers_for_filling_dont_interrupt() { + let actual = invoke_cascade(["fail", "custom-helper"], action_get()) + .unwrap() + .expect("credentials"); + assert_eq!(actual.identity, identity("user-script", "pass-script")); + } + + #[test] + fn urls_are_split_in_get_to_support_scripts() { + let actual = invoke_cascade( + ["reflect", "custom-helper"], + Action::get_for_url("https://example.com:8080/path/git/"), + ) + .unwrap() + .expect("credentials"); + + let ctx: Context = (&actual.next).try_into().unwrap(); + assert_eq!(ctx.protocol.as_deref().expect("protocol"), "https"); + assert_eq!(ctx.host.as_deref().expect("host"), "example.com:8080"); + assert_eq!(ctx.path.as_deref().expect("path").as_bstr(), "path/git"); + } + + #[test] + fn urls_are_split_in_get_but_can_skip_the_path_in_host_only_urls() { + let actual = invoke_cascade(["reflect", "custom-helper"], Action::get_for_url("http://example.com")) + .unwrap() + .expect("credentials"); + + let ctx: Context = (&actual.next).try_into().unwrap(); + assert_eq!(ctx.protocol.as_deref().expect("protocol"), "http"); + assert_eq!(ctx.host.as_deref().expect("host"), "example.com"); + assert_eq!(ctx.path, None); + } + + #[test] + fn helpers_can_set_any_context_value() { + let actual = invoke_cascade( + ["all-but-credentials", "custom-helper"], + Action::get_for_url("http://github.com"), + ) + .unwrap() + .expect("credentials"); + + let ctx: Context = (&actual.next).try_into().unwrap(); + assert_eq!(ctx.protocol.as_deref().expect("protocol"), "ftp"); + assert_eq!(ctx.host.as_deref().expect("host"), "example.com:8080"); + assert_eq!( + ctx.path.expect("set by helper"), + "/path/to/git/", + "values are passed verbatim even if they would otherwise look different" + ); + } + + #[test] + fn helpers_can_set_any_context_value_using_the_url_only() { + let actual = invoke_cascade(["url", "custom-helper"], Action::get_for_url("http://github.com")) + .unwrap() + .expect("credentials"); + + let ctx: Context = (&actual.next).try_into().unwrap(); + assert_eq!( + ctx.protocol.as_deref().expect("protocol"), + "http", + "url is processed last, it overwrites what came before" + ); + assert_eq!(ctx.host.as_deref().expect("host"), "example.com:8080"); + assert_eq!( + ctx.path.expect("set by helper"), + "path/to/git", + "the url is processed like any other" + ); + } + + #[test] + fn helpers_can_quit_and_their_creds_are_taken_if_complete() { + let actual = invoke_cascade(["last-pass", "custom-helper"], Action::get_for_url("http://github.com")) + .unwrap() + .expect("credentials"); + + assert_eq!(actual.identity, identity("user", "pass")); + } + + #[test] + fn bogus_password_overrides_any_helper_and_helper_overrides_username_in_url() { + let actual = Cascade::default() + .query_user_only(true) + .extend(fixtures(["username", "password"])) + .invoke( + Action::get_for_url("ssh://git@host/repo"), + gix_prompt::Options { + mode: gix_prompt::Mode::Disable, + askpass: None, + }, + ) + .unwrap() + .expect("credentials"); + assert_eq!(actual.identity, identity("user", "")); + } + + fn action_get() -> Action { + Action::get_for_url("does/not/matter") + } + + fn identity(user: &str, pass: &str) -> Account { + Account { + username: user.into(), + password: pass.into(), + } + } + + #[allow(clippy::result_large_err)] + fn invoke_cascade<'a>(names: impl IntoIterator<Item = &'a str>, action: Action) -> protocol::Result { + Cascade::default().use_http_path(true).extend(fixtures(names)).invoke( + action, + gix_prompt::Options { + mode: gix_prompt::Mode::Disable, + askpass: None, + }, + ) + } + + fn fixtures<'a>(names: impl IntoIterator<Item = &'a str>) -> Vec<Program> { + names + .into_iter() + .map(|name| gix_path::realpath(fixture_path(format!("{name}.sh"))).unwrap()) + .map(|path| { + let mut script = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(path)).into_owned(); + script.insert_str(0, "sh "); + Program::from_kind(program::Kind::ExternalShellScript(script)) + }) + .collect() + } +} diff --git a/vendor/gix-credentials/tests/helper/context.rs b/vendor/gix-credentials/tests/helper/context.rs new file mode 100644 index 000000000..6566c6c2b --- /dev/null +++ b/vendor/gix-credentials/tests/helper/context.rs @@ -0,0 +1,123 @@ +use gix_credentials::protocol::Context; + +#[test] +fn encode_decode_roundtrip_works_only_for_serializing_fields() { + for ctx in [ + Context { + protocol: Some("https".into()), + host: Some("github.com".into()), + path: Some("byron/gitoxide".into()), + username: Some("user".into()), + password: Some("pass".into()), + url: Some("https://github.com/byron/gitoxide".into()), + ..Default::default() + }, + Context::default(), + ] { + let mut buf = Vec::<u8>::new(); + ctx.write_to(&mut buf).unwrap(); + let actual = Context::from_bytes(&buf).unwrap(); + assert_eq!(actual, ctx, "ctx should encode itself losslessly"); + } +} + +mod write_to { + use gix_credentials::protocol::Context; + + #[test] + fn quit_is_not_serialized_but_can_be_parsed() { + let mut buf = Vec::<u8>::new(); + Context { + quit: Some(true), + ..Default::default() + } + .write_to(&mut buf) + .unwrap(); + assert_eq!(Context::from_bytes(&buf).unwrap(), Context::default()); + assert_eq!( + Context::from_bytes(b"quit=true\nurl=https://example.com").unwrap(), + Context { + quit: Some(true), + url: Some("https://example.com".into()), + ..Default::default() + } + ); + } + + #[test] + fn null_bytes_and_newlines_are_invalid() { + for input in [&b"foo\0"[..], b"foo\n"] { + let ctx = Context { + path: Some(input.into()), + ..Default::default() + }; + let mut buf = Vec::<u8>::new(); + let err = ctx.write_to(&mut buf).unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::Other); + } + } +} + +mod from_bytes { + use gix_credentials::protocol::Context; + + #[test] + fn empty_newlines_cause_skipping_remaining_input() { + let input = b"protocol=https +host=example.com\n +password=secr3t +username=bob"; + assert_eq!( + Context::from_bytes(input).unwrap(), + Context { + protocol: Some("https".into()), + host: Some("example.com".into()), + ..Default::default() + } + ) + } + + #[test] + fn unknown_field_names_are_skipped() { + let input = b"protocol=https +unknown=value +username=bob"; + assert_eq!( + Context::from_bytes(input).unwrap(), + Context { + protocol: Some("https".into()), + username: Some("bob".into()), + ..Default::default() + } + ) + } + + #[test] + fn quit_supports_git_config_boolean_values() { + for true_value in ["1", "42", "-42", "true", "on", "yes"] { + let input = format!("quit={true_value}"); + assert_eq!( + Context::from_bytes(input.as_bytes()).unwrap().quit, + Some(true), + "{input}" + ) + } + for false_value in ["0", "false", "off", "no"] { + let input = format!("quit={false_value}"); + assert_eq!( + Context::from_bytes(input.as_bytes()).unwrap().quit, + Some(false), + "{input}" + ) + } + } + + #[test] + fn null_bytes_when_decoding() { + let err = Context::from_bytes(b"url=https://foo\0").unwrap_err(); + assert!(matches!( + err, + gix_credentials::protocol::context::decode::Error::Encoding(_) + )); + } +} diff --git a/vendor/gix-credentials/tests/helper/invoke.rs b/vendor/gix-credentials/tests/helper/invoke.rs new file mode 100644 index 000000000..330048548 --- /dev/null +++ b/vendor/gix-credentials/tests/helper/invoke.rs @@ -0,0 +1,134 @@ +use bstr::{BString, ByteVec}; +use gix_credentials::{helper, protocol::Context, Program}; +use gix_testtools::fixture_path; + +#[test] +fn get() { + let mut outcome = gix_credentials::helper::invoke( + &mut script_helper("last-pass"), + &helper::Action::get_for_url("https://github.com/byron/gitoxide"), + ) + .unwrap() + .expect("mock provides credentials"); + assert_eq!( + outcome.consume_identity().expect("complete"), + gix_sec::identity::Account { + username: "user".into(), + password: "pass".into() + } + ); + assert_eq!( + outcome.next.store().payload().unwrap(), + "username=user\npassword=pass\nquit=1\n" + ); +} + +#[test] +fn store_and_reject() { + let ctx = Context { + url: Some("https://github.com/byron/gitoxide".into()), + ..Default::default() + }; + let ctxbuf = || -> BString { + let mut buf = Vec::<u8>::new(); + ctx.write_to(&mut buf).expect("cannot fail"); + buf.into() + }; + for action in [helper::Action::Store(ctxbuf()), helper::Action::Erase(ctxbuf())] { + let outcome = gix_credentials::helper::invoke(&mut script_helper("last-pass"), &action).unwrap(); + assert!( + outcome.is_none(), + "store and erase have no outcome, they just shouldn't fail" + ); + } +} + +mod program { + use gix_credentials::{helper, program::Kind, Program}; + + use crate::helper::invoke::script_helper; + + #[test] + fn builtin() { + assert!( + matches!( + gix_credentials::helper::invoke( + &mut Program::from_kind(Kind::Builtin).suppress_stderr(), + &helper::Action::get_for_url("/path/without/scheme/fails/with/error"), + ) + .unwrap_err(), + helper::Error::CredentialsHelperFailed { .. } + ), + "this failure indicates we could launch the helper, even though it wasn't happy which is fine. It doesn't like the URL" + ); + } + + #[test] + fn script() { + assert_eq!( + gix_credentials::helper::invoke( + &mut Program::from_custom_definition( + "!f() { test \"$1\" = get && echo \"password=pass\" && echo \"username=user\"; }; f" + ), + &helper::Action::get_for_url("/does/not/matter"), + ) + .unwrap() + .expect("present") + .consume_identity() + .expect("complete"), + gix_sec::identity::Account { + username: "user".into(), + password: "pass".into() + } + ); + } + + #[cfg(unix)] // needs executable bits to work + #[test] + fn path_to_helper_script() -> crate::Result { + assert_eq!( + gix_credentials::helper::invoke( + &mut Program::from_custom_definition( + gix_path::into_bstr(gix_path::realpath(gix_testtools::fixture_path("custom-helper.sh"))?) + .into_owned() + ), + &helper::Action::get_for_url("/does/not/matter"), + )? + .expect("present") + .consume_identity() + .expect("complete"), + gix_sec::identity::Account { + username: "user-script".into(), + password: "pass-script".into() + } + ); + Ok(()) + } + + #[test] + fn path_to_helper_as_script_to_workaround_executable_bits() -> crate::Result { + assert_eq!( + gix_credentials::helper::invoke( + &mut script_helper("custom-helper"), + &helper::Action::get_for_url("/does/not/matter") + )? + .expect("present") + .consume_identity() + .expect("complete"), + gix_sec::identity::Account { + username: "user-script".into(), + password: "pass-script".into() + } + ); + Ok(()) + } +} + +pub fn script_helper(name: &str) -> Program { + let mut script = gix_path::to_unix_separators_on_windows(gix_path::into_bstr( + gix_path::realpath(fixture_path(format!("{name}.sh"))).unwrap(), + )) + .into_owned(); + script.insert_str(0, "sh "); + Program::from_kind(gix_credentials::program::Kind::ExternalShellScript(script)) +} diff --git a/vendor/gix-credentials/tests/helper/mod.rs b/vendor/gix-credentials/tests/helper/mod.rs new file mode 100644 index 000000000..59c448065 --- /dev/null +++ b/vendor/gix-credentials/tests/helper/mod.rs @@ -0,0 +1,39 @@ +mod cascade; +mod context; +mod invoke; + +mod invoke_outcome_to_helper_result { + use gix_credentials::{helper, protocol, protocol::helper_outcome_to_result}; + + #[test] + fn missing_username_or_password_causes_failure_with_get_action() { + let action = helper::Action::get_for_url("does/not/matter"); + let err = helper_outcome_to_result( + Some(helper::Outcome { + username: None, + password: None, + quit: false, + next: protocol::Context::default().into(), + }), + action, + ) + .unwrap_err(); + assert!(matches!(err, protocol::Error::IdentityMissing { .. })); + } + + #[test] + fn quit_message_in_context_causes_special_error_ignoring_missing_identity() { + let action = helper::Action::get_for_url("does/not/matter"); + let err = helper_outcome_to_result( + Some(helper::Outcome { + username: None, + password: None, + quit: true, + next: protocol::Context::default().into(), + }), + action, + ) + .unwrap_err(); + assert!(matches!(err, protocol::Error::Quit)); + } +} diff --git a/vendor/gix-credentials/tests/program/from_custom_definition.rs b/vendor/gix-credentials/tests/program/from_custom_definition.rs new file mode 100644 index 000000000..1b9d8a056 --- /dev/null +++ b/vendor/gix-credentials/tests/program/from_custom_definition.rs @@ -0,0 +1,42 @@ +use gix_credentials::{program::Kind, Program}; + +#[test] +fn script() { + assert!( + matches!(Program::from_custom_definition("!exe").kind, Kind::ExternalShellScript(script) if script == "exe") + ); +} + +#[test] +fn name_with_args() { + let input = "name --arg --bar=\"a b\""; + let expected = "git credential-name --arg --bar=\"a b\""; + assert!( + matches!(Program::from_custom_definition(input).kind, Kind::ExternalName{name_and_args} if name_and_args == expected) + ); +} + +#[test] +fn name() { + let input = "name"; + let expected = "git credential-name"; + assert!( + matches!(Program::from_custom_definition(input).kind, Kind::ExternalName{name_and_args} if name_and_args == expected) + ); +} + +#[test] +fn path_with_args() { + let input = "/abs/name --arg --bar=\"a b\""; + assert!( + matches!(Program::from_custom_definition(input).kind, Kind::ExternalPath{path_and_args} if path_and_args == input) + ); +} + +#[test] +fn path() { + let input = "/abs/name"; + assert!( + matches!(Program::from_custom_definition(input).kind, Kind::ExternalPath{path_and_args} if path_and_args == input) + ); +} diff --git a/vendor/gix-credentials/tests/program/mod.rs b/vendor/gix-credentials/tests/program/mod.rs new file mode 100644 index 000000000..3672dd18e --- /dev/null +++ b/vendor/gix-credentials/tests/program/mod.rs @@ -0,0 +1 @@ +mod from_custom_definition; diff --git a/vendor/gix-credentials/tests/protocol/context.rs b/vendor/gix-credentials/tests/protocol/context.rs new file mode 100644 index 000000000..3cfd850a3 --- /dev/null +++ b/vendor/gix-credentials/tests/protocol/context.rs @@ -0,0 +1,175 @@ +mod destructure_url_in_place { + use gix_credentials::protocol::Context; + + fn url_ctx(url: &str) -> Context { + Context { + url: Some(url.into()), + ..Default::default() + } + } + + fn assert_eq_parts( + url: &str, + proto: &str, + user: impl Into<Option<&'static str>>, + host: &str, + path: impl Into<Option<&'static str>>, + use_http_path: bool, + ) { + let mut ctx = url_ctx(url); + ctx.destructure_url_in_place(use_http_path).expect("splitting works"); + assert_eq!(ctx.protocol.expect("set proto"), proto); + match user.into() { + Some(expected) => assert_eq!(ctx.username.expect("set user"), expected), + None => assert!(ctx.username.is_none()), + } + assert_eq!(ctx.host.expect("set host"), host); + match path.into() { + Some(expected) => assert_eq!(ctx.path.expect("set path"), expected), + None => assert!(ctx.path.is_none()), + } + } + + #[test] + fn parts_are_verbatim_with_non_http_url() { + // path is always used for non-http + assert_eq_parts("ssh://user@host:21/path", "ssh", "user", "host:21", "path", false); + assert_eq_parts("ssh://host.org/path", "ssh", None, "host.org", "path", true); + } + #[test] + fn http_and_https_ignore_the_path_by_default() { + assert_eq_parts( + "http://user@example.com/path", + "http", + Some("user"), + "example.com", + None, + false, + ); + assert_eq_parts( + "https://github.com/byron/gitoxide", + "https", + None, + "github.com", + None, + false, + ); + assert_eq_parts( + "https://github.com/byron/gitoxide/", + "https", + None, + "github.com", + "byron/gitoxide", + true, + ); + } +} + +mod to_prompt { + use gix_credentials::protocol::Context; + + #[test] + fn no_scheme_means_no_url() { + assert_eq!(Context::default().to_prompt("Username"), "Username: "); + } + + #[test] + fn any_scheme_means_url_is_included() { + assert_eq!( + Context { + protocol: Some("https".into()), + host: Some("host".into()), + ..Default::default() + } + .to_prompt("Password"), + "Password for https://host: " + ); + } +} + +mod to_url { + use gix_credentials::protocol::Context; + + #[test] + fn no_protocol_is_nothing() { + assert_eq!(Context::default().to_url(), None); + } + #[test] + fn protocol_alone_is_enough() { + assert_eq!( + Context { + protocol: Some("https".into()), + ..Default::default() + } + .to_url() + .unwrap(), + "https://" + ); + } + #[test] + fn username_is_appended() { + assert_eq!( + Context { + protocol: Some("https".into()), + username: Some("user".into()), + ..Default::default() + } + .to_url() + .unwrap(), + "https://user@" + ); + } + #[test] + fn host_is_appended() { + assert_eq!( + Context { + protocol: Some("https".into()), + host: Some("host".into()), + ..Default::default() + } + .to_url() + .unwrap(), + "https://host" + ); + } + #[test] + fn path_is_appended_with_leading_slash_placed_as_needed() { + assert_eq!( + Context { + protocol: Some("file".into()), + path: Some("dir/git".into()), + ..Default::default() + } + .to_url() + .unwrap(), + "file:///dir/git" + ); + assert_eq!( + Context { + protocol: Some("file".into()), + path: Some("/dir/git".into()), + ..Default::default() + } + .to_url() + .unwrap(), + "file:///dir/git" + ); + } + + #[test] + fn all_fields_with_port_but_password_is_never_shown() { + assert_eq!( + Context { + protocol: Some("https".into()), + username: Some("user".into()), + password: Some("secret".into()), + host: Some("example.com:8080".into()), + path: Some("Byron/gitoxide".into()), + ..Default::default() + } + .to_url() + .unwrap(), + "https://user@example.com:8080/Byron/gitoxide" + ); + } +} diff --git a/vendor/gix-credentials/tests/protocol/mod.rs b/vendor/gix-credentials/tests/protocol/mod.rs new file mode 100644 index 000000000..3d9885db8 --- /dev/null +++ b/vendor/gix-credentials/tests/protocol/mod.rs @@ -0,0 +1 @@ +mod context; |