use cascade_cli::bitbucket::{BitbucketClient, PullRequestManager};
use cascade_cli::config::BitbucketConfig;
use serde_json::json;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use tempfile::TempDir;
#[tokio::test]
async fn test_api_rate_limiting_behavior() {
let mut server = mockito::Server::new_async().await;
let _rate_limit_mock = server
.mock("GET", "/rest/api/1.0/projects/TEST/repos/test-repo")
.with_status(429)
.with_header("content-type", "application/json")
.with_header("Retry-After", "1")
.with_body(
json!({
"errors": [{
"message": "Too many requests"
}]
})
.to_string(),
)
.expect(1)
.create_async()
.await;
let _success_mock = server
.mock("GET", "/rest/api/1.0/projects/TEST/repos/test-repo")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"id": 1,
"name": "test-repo",
"slug": "test-repo",
"project": {
"id": 1,
"key": "TEST",
"name": "Test Project"
}
})
.to_string(),
)
.create_async()
.await;
let config = BitbucketConfig {
url: server.url(),
project: "TEST".to_string(),
repo: "test-repo".to_string(),
username: Some("testuser".to_string()),
token: Some("testtoken".to_string()),
default_reviewers: vec![],
accept_invalid_certs: None,
ca_bundle_path: None,
};
let client = BitbucketClient::new(&config).unwrap();
let start_time = std::time::Instant::now();
let result = client.test_connection().await;
let elapsed = start_time.elapsed();
match result {
Ok(_) => {
assert!(
elapsed >= Duration::from_millis(100),
"Should have taken time to retry"
);
}
Err(e) => {
let error_msg = e.to_string();
assert!(
error_msg.contains("429")
|| error_msg.contains("rate limit")
|| error_msg.contains("Too many"),
"Should contain rate limit error: {error_msg}"
);
}
}
}
#[tokio::test]
async fn test_network_timeout_handling() {
let mut server = mockito::Server::new_async().await;
let _slow_mock = server
.mock("GET", "/rest/api/1.0/projects/TEST/repos/test-repo")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"id": 1,
"name": "test-repo"
})
.to_string(),
)
.create_async()
.await;
let config = BitbucketConfig {
url: server.url(),
project: "TEST".to_string(),
repo: "test-repo".to_string(),
username: Some("testuser".to_string()),
token: Some("testtoken".to_string()),
default_reviewers: vec![],
accept_invalid_certs: None,
ca_bundle_path: None,
};
let client = BitbucketClient::new(&config).unwrap();
let result = tokio::time::timeout(Duration::from_millis(100), client.test_connection()).await;
match result {
Ok(connection_result) => {
match connection_result {
Err(e) => {
println!("Connection failed within timeout (expected): {e:?}");
}
Ok(_) => {
println!("Connection succeeded within timeout");
}
}
}
Err(_) => {
println!("Connection timed out (this tests our timeout wrapper)");
}
}
}
#[tokio::test]
async fn test_authentication_token_expiration() {
let mut server = mockito::Server::new_async().await;
let _auth_expired_mock = server
.mock("GET", "/rest/api/1.0/projects/TEST/repos/test-repo")
.with_status(401)
.with_header("content-type", "application/json")
.with_body(
json!({
"errors": [{
"message": "Authentication failed",
"exceptionName": "com.atlassian.bitbucket.AuthorisationException"
}]
})
.to_string(),
)
.create_async()
.await;
let config = BitbucketConfig {
url: server.url(),
project: "TEST".to_string(),
repo: "test-repo".to_string(),
username: Some("testuser".to_string()),
token: Some("expired-token".to_string()),
default_reviewers: vec![],
accept_invalid_certs: None,
ca_bundle_path: None,
};
let client = BitbucketClient::new(&config).unwrap();
let result = client.test_connection().await;
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("401")
|| error_msg.contains("Authentication")
|| error_msg.contains("Unauthor"),
"Should contain authentication error: {error_msg}"
);
}
#[tokio::test]
async fn test_partial_api_operation_failures() {
let mut server = mockito::Server::new_async().await;
let _repo_mock = server
.mock("GET", "/rest/api/1.0/projects/TEST/repos/test-repo")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"id": 1,
"name": "test-repo",
"slug": "test-repo",
"project": {
"id": 1,
"key": "TEST",
"name": "Test Project"
}
})
.to_string(),
)
.create_async()
.await;
let _pr_fail_mock = server
.mock(
"POST",
"/rest/api/1.0/projects/TEST/repos/test-repo/pull-requests",
)
.with_status(500)
.with_header("content-type", "application/json")
.with_body(
json!({
"errors": [{
"message": "Internal server error during PR creation"
}]
})
.to_string(),
)
.create_async()
.await;
let config = BitbucketConfig {
url: server.url(),
project: "TEST".to_string(),
repo: "test-repo".to_string(),
username: Some("testuser".to_string()),
token: Some("testtoken".to_string()),
default_reviewers: vec![],
accept_invalid_certs: None,
ca_bundle_path: None,
};
let client = BitbucketClient::new(&config).unwrap();
let connection_result = client.test_connection().await;
assert!(connection_result.is_ok(), "Repo access should succeed");
let _pr_manager = PullRequestManager::new(client);
println!("PR manager created successfully, ready for partial operation testing");
}
#[tokio::test]
async fn test_network_interruption_scenarios() {
let config = BitbucketConfig {
url: "https://unreachable.invalid.domain.test".to_string(),
project: "TEST".to_string(),
repo: "test-repo".to_string(),
username: Some("testuser".to_string()),
token: Some("testtoken".to_string()),
default_reviewers: vec![],
accept_invalid_certs: None,
ca_bundle_path: None,
};
let client_result = BitbucketClient::new(&config);
assert!(
client_result.is_ok(),
"Client creation should succeed with invalid URL"
);
let client = client_result.unwrap();
let connection_result = client.test_connection().await;
assert!(
connection_result.is_err(),
"Connection to invalid domain should fail"
);
let error_msg = connection_result.unwrap_err().to_string();
assert!(
error_msg.contains("dns")
|| error_msg.contains("resolve")
|| error_msg.contains("network")
|| error_msg.contains("connection")
|| error_msg.contains("timeout")
|| error_msg.contains("eof")
|| error_msg.contains("tunneling"),
"Should contain network-related error: {error_msg}"
);
}
#[tokio::test]
async fn test_cli_network_error_integration() {
let (_temp_dir, repo_path) = create_test_git_repo().await;
cascade_cli::config::initialize_repo(
&repo_path,
Some("https://unreachable.bitbucket.test".to_string()),
)
.unwrap();
let binary_path = super::test_helpers::get_binary_path();
let stack_result = Command::new(&binary_path)
.args(["stacks", "create", "network-test"])
.current_dir(&repo_path)
.output()
.expect("Command should run");
if !stack_result.status.success() {
let stderr = String::from_utf8_lossy(&stack_result.stderr);
println!("Stack creation failed: {stderr}");
}
super::test_helpers::create_test_commits(&repo_path, 1, "network-test").await;
let push_result = Command::new(&binary_path)
.args(["push", "--yes"])
.current_dir(&repo_path)
.output()
.expect("Command should run");
if !push_result.status.success() {
let stderr = String::from_utf8_lossy(&push_result.stderr);
let stdout = String::from_utf8_lossy(&push_result.stdout);
assert!(
stderr.contains("network")
|| stderr.contains("connection")
|| stderr.contains("resolve")
|| stderr.contains("timeout")
|| stderr.contains("unreachable")
|| stderr.contains("Configuration")
|| stderr.contains("uncommitted")
|| stderr.contains("Error")
|| stdout.contains("Error"),
"Should contain some kind of graceful error. Stderr: {stderr}, Stdout: {stdout}"
);
} else {
let stdout = String::from_utf8_lossy(&push_result.stdout);
println!("Push unexpectedly succeeded: {stdout}");
}
}
async fn create_test_git_repo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
Command::new("git")
.args(["init"])
.current_dir(&repo_path)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(&repo_path)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(&repo_path)
.output()
.unwrap();
std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(&repo_path)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "Initial"])
.current_dir(&repo_path)
.output()
.unwrap();
(temp_dir, repo_path)
}