aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/tests/common/io_stub.rs32
-rw-r--r--crates/shirabe/tests/util/git_test.rs176
2 files changed, 203 insertions, 5 deletions
diff --git a/crates/shirabe/tests/common/io_stub.rs b/crates/shirabe/tests/common/io_stub.rs
index 9318d45..b57dd0c 100644
--- a/crates/shirabe/tests/common/io_stub.rs
+++ b/crates/shirabe/tests/common/io_stub.rs
@@ -29,6 +29,10 @@ pub struct IOStub {
ask: Option<PhpMixed>,
ask_confirmation: Option<bool>,
ask_and_hide_answer: Option<Option<String>>,
+ // Keyed `askAndHideAnswer` replies, mirroring tests whose willReturnCallback
+ // switches on the question string. Unknown questions return `''` like PHP's
+ // `switch` default.
+ ask_and_hide_answer_responses: Option<indexmap::IndexMap<String, String>>,
}
impl IOStub {
@@ -79,6 +83,29 @@ impl IOStub {
self.ask_and_hide_answer = Some(value);
self
}
+ pub fn with_ask_and_hide_answer_responses(
+ mut self,
+ value: indexmap::IndexMap<String, String>,
+ ) -> Self {
+ self.ask_and_hide_answer_responses = Some(value);
+ self
+ }
+
+ // Pre-seeds the in-memory auth store, equivalent to a willReturnCallback that
+ // reads a captured `$initial_config`. Reads delegate to `BaseIO` as long as the
+ // `with_has_authentication`/`with_get_authentication` overrides are left unset.
+ pub fn with_authentication(
+ mut self,
+ repository_name: impl Into<String>,
+ username: impl Into<String>,
+ password: Option<String>,
+ ) -> Self {
+ let mut auth = indexmap::IndexMap::new();
+ auth.insert("username".to_string(), Some(username.into()));
+ auth.insert("password".to_string(), password);
+ self.authentications.insert(repository_name.into(), auth);
+ self
+ }
}
impl IOInterfaceImmutable for IOStub {
@@ -127,7 +154,10 @@ impl IOInterfaceImmutable for IOStub {
) -> anyhow::Result<PhpMixed> {
Ok(default)
}
- fn ask_and_hide_answer(&self, _question: String) -> Option<String> {
+ fn ask_and_hide_answer(&self, question: String) -> Option<String> {
+ if let Some(responses) = &self.ask_and_hide_answer_responses {
+ return Some(responses.get(&question).cloned().unwrap_or_default());
+ }
self.ask_and_hide_answer.clone().unwrap_or(None)
}
fn select(
diff --git a/crates/shirabe/tests/util/git_test.rs b/crates/shirabe/tests/util/git_test.rs
index 7e382b5..80037da 100644
--- a/crates/shirabe/tests/util/git_test.rs
+++ b/crates/shirabe/tests/util/git_test.rs
@@ -4,17 +4,73 @@ use std::cell::RefCell;
use std::rc::Rc;
use indexmap::IndexMap;
-use shirabe::config::Config;
+use shirabe::config::{Config, ConfigSourceInterface};
use shirabe::io::IOInterface;
use shirabe::util::filesystem::Filesystem;
use shirabe::util::git::Git;
+use shirabe::util::http_downloader::HttpDownloaderMockHandler;
use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor};
use shirabe_php_shim::{PhpMixed, RuntimeException};
use crate::config_stub::ConfigStubBuilder;
+use crate::http_downloader_mock::{expect_full, get_http_downloader_mock};
use crate::io_stub::IOStub;
use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
+// No-op ConfigSourceInterface, equivalent to PHPUnit's
+// `getMockBuilder(Config::class)` auto-stubbing getConfigSource/getAuthConfigSource:
+// the Bitbucket OAuth flow writes the token through these, but GitTest never asserts
+// on them.
+#[derive(Debug)]
+struct NullConfigSource;
+
+impl ConfigSourceInterface for NullConfigSource {
+ fn add_repository(
+ &mut self,
+ _name: &str,
+ _config: PhpMixed,
+ _append: bool,
+ ) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn insert_repository(
+ &mut self,
+ _name: &str,
+ _config: PhpMixed,
+ _reference_name: &str,
+ _offset: i64,
+ ) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn add_link(&mut self, _type: &str, _name: &str, _value: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn remove_link(&mut self, _type: &str, _name: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn get_name(&self) -> String {
+ "null".to_string()
+ }
+}
+
// PHP's `commandCallable` returns a bare string (`'git command'`); Rust's `run_command`
// flattens each callable to a `Vec<String>` and hands it to `execute_args`, which always
// builds a `PhpMixed::List`. So the single-token string command becomes a one-element list,
@@ -351,10 +407,122 @@ fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interacti
);
}
+// privateBitbucketWithOauthProvider helper.
+fn run_command_private_bitbucket_interactive_with_oauth(
+ git_url: &str,
+ expected_url: &str,
+ initial_config: Option<(&str, &str)>,
+) {
+ let expected_url_owned = expected_url.to_string();
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ if url != expected_url_owned {
+ return vec!["git command failing".to_string()];
+ }
+ vec!["git command ok".to_string()]
+ });
+
+ let mut config = ConfigStubBuilder::new()
+ .with(
+ "gitlab-domains",
+ PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]),
+ )
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .build();
+ config.set_config_source(Box::new(NullConfigSource));
+ config.set_auth_config_source(Box::new(NullConfigSource));
+
+ let mut expected_calls: Vec<MockExpectation> = Vec::new();
+ expected_calls.push(cmd_full(vec!["git command failing"], 1, "", ""));
+ if initial_config.is_some() {
+ expected_calls.push(cmd_full(vec!["git command failing"], 1, "", ""));
+ } else {
+ expected_calls.push(cmd_full(
+ vec!["git", "config", "bitbucket.accesstoken"],
+ 1,
+ "",
+ "",
+ ));
+ }
+ expected_calls.push(cmd_full(vec!["git command ok"], 0, "", ""));
+
+ let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default());
+
+ let mut hidden_answers: IndexMap<String, String> = IndexMap::new();
+ hidden_answers.insert(
+ "Consumer Key (hidden): ".to_string(),
+ "my-consumer-key".to_string(),
+ );
+ hidden_answers.insert(
+ "Consumer Secret (hidden): ".to_string(),
+ "my-consumer-secret".to_string(),
+ );
+
+ let mut io = IOStub::new()
+ .with_is_interactive(true)
+ .with_ask_confirmation(true)
+ .with_ask_and_hide_answer_responses(hidden_answers);
+ if let Some((username, password)) = initial_config {
+ io = io.with_authentication("bitbucket.org", username, Some(password.to_string()));
+ }
+
+ let (downloader, _http_guard) = get_http_downloader_mock(
+ vec![expect_full(
+ "https://bitbucket.org/site/oauth2/access_token",
+ None,
+ 200,
+ r#"{"expires_in": 600, "access_token": "my-access-token"}"#,
+ vec![],
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let mut git = build_git(io, config, process);
+ git.set_http_downloader(downloader);
+
+ git.__run_command(vec![command_callable], git_url, None, true, None)
+ .unwrap();
+}
+
#[test]
-#[ignore = "interactive Bitbucket OAuth flow needs IOStub::askConfirmation/askAndHideAnswer/setAuthentication stateful callbacks and getHttpDownloaderMock; IOStub only supports fixed willReturn values, not the willReturnCallback-based stateful initial_config mutation this test relies on"]
-fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth() {
- todo!()
+fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth_ssh() {
+ run_command_private_bitbucket_interactive_with_oauth(
+ "git@bitbucket.org:acme/repo.git",
+ "https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git",
+ None,
+ );
+}
+
+#[test]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth_https_git()
+ {
+ run_command_private_bitbucket_interactive_with_oauth(
+ "https://bitbucket.org/acme/repo.git",
+ "https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git",
+ None,
+ );
+}
+
+#[test]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth_https() {
+ run_command_private_bitbucket_interactive_with_oauth(
+ "https://bitbucket.org/acme/repo",
+ "https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git",
+ None,
+ );
+}
+
+#[test]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth_preconfigured()
+ {
+ run_command_private_bitbucket_interactive_with_oauth(
+ "git@bitbucket.org:acme/repo.git",
+ "https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git",
+ Some(("someuseralsoswappedfortoken", "little green men")),
+ );
}
#[test]