use bytes::Bytes;
use reqwest::header::HeaderMap;
use crate::Client;
use crate::Response;
use crate::response::response_from_reqwest;
impl Client {
fn prepare_request(
&self,
method: &reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
) -> crate::Result<(reqwest::Client, reqwest::RequestBuilder, String, bool)> {
let http_client = self.http_client();
let (base_url, access_token, otp, username, password, sudo, user_agent, debug) = {
let config = self.read_config();
(
config.base_url.clone(),
config.access_token.clone(),
config.otp.clone(),
config.username.clone(),
config.password.clone(),
config.sudo.clone(),
config.user_agent.clone(),
config.debug,
)
};
let url = format!("{base_url}/api/v1{path}");
let mut req = http_client
.request(method.clone(), &url)
.header("Accept", "application/json");
if !access_token.is_empty() {
req = req.header("Authorization", format!("token {access_token}"));
}
if !otp.is_empty() {
req = req.header("X-GITEA-OTP", &*otp);
}
if !username.is_empty() {
req = req.basic_auth(&*username, Some(&*password));
}
if !sudo.is_empty() {
req = req.header("Sudo", &*sudo);
}
if !user_agent.is_empty() {
req = req.header("User-Agent", &*user_agent);
}
if let Some(hdrs) = headers {
for (k, v) in hdrs.iter() {
req = req.header(k, v);
}
}
Ok((http_client, req, url, debug))
}
fn finish_request(
&self,
http_client: reqwest::Client,
req: reqwest::RequestBuilder,
method: &reqwest::Method,
url: &str,
debug: bool,
) -> crate::Result<(reqwest::Client, reqwest::Request)> {
if debug {
tracing::debug!("{}: {}", method, url);
}
let mut built_req = req.build()?;
{
let signer = self.ssh_signer();
if let Some(ref signer) = *signer {
let use_legacy = self.should_use_legacy_ssh();
crate::auth::ssh_sign::sign_request(&mut built_req, signer, use_legacy)?;
}
}
Ok((http_client, built_req))
}
async fn do_request_raw<B: Into<reqwest::Body>>(
&self,
method: reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
body: Option<B>,
) -> crate::Result<reqwest::Response> {
let (http_client, req, url, debug) = self.prepare_request(&method, path, headers)?;
let req = if let Some(b) = body { req.body(b) } else { req };
let (http_client, built_req) =
self.finish_request(http_client, req, &method, &url, debug)?;
let resp = http_client.execute(built_req).await?;
Ok(resp)
}
pub(crate) async fn do_request_with_status_handle<B: Into<reqwest::Body>>(
&self,
method: reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
body: Option<B>,
) -> crate::Result<Response> {
let resp = self.do_request_raw(method, path, headers, body).await?;
let response = response_from_reqwest(&resp);
let status = resp.status().as_u16();
if status / 100 != 2 {
let err_bytes = resp.bytes().await.unwrap_or_default();
status_code_to_err(status, &err_bytes)?;
}
Ok(response)
}
pub(crate) async fn get_status_code<B: Into<reqwest::Body>>(
&self,
method: reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
body: Option<B>,
) -> crate::Result<(u16, Response)> {
let resp = self.do_request_raw(method, path, headers, body).await?;
let response = response_from_reqwest(&resp);
let status = resp.status().as_u16();
Ok((status, response))
}
pub(crate) async fn get_response<B: Into<reqwest::Body>>(
&self,
method: reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
body: Option<B>,
) -> crate::Result<(Bytes, Response)> {
let resp = self.do_request_raw(method, path, headers, body).await?;
let response = response_from_reqwest(&resp);
let status = resp.status().as_u16();
if status / 100 != 2 {
let err_bytes = resp.bytes().await.unwrap_or_default();
status_code_to_err(status, &err_bytes)?;
unreachable!()
}
let data = resp.bytes().await?;
Ok((data, response))
}
pub(crate) async fn get_parsed_response<
T: serde::de::DeserializeOwned,
B: Into<reqwest::Body>,
>(
&self,
method: reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
body: Option<B>,
) -> crate::Result<(T, Response)> {
let (data, response) = self.get_response(method, path, headers, body).await?;
let value: T = serde_json::from_slice(&data)?;
Ok((value, response))
}
pub(crate) async fn get_parsed_response_multipart<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
headers: Option<&HeaderMap>,
form: reqwest::multipart::Form,
) -> crate::Result<(T, Response)> {
let (http_client, req, url, debug) = self.prepare_request(&method, path, headers)?;
let (http_client, built_req) =
self.finish_request(http_client, req.multipart(form), &method, &url, debug)?;
let resp = http_client.execute(built_req).await?;
let response = response_from_reqwest(&resp);
let status = resp.status().as_u16();
if status / 100 != 2 {
let err_bytes = resp.bytes().await.unwrap_or_default();
status_code_to_err(status, &err_bytes)?;
unreachable!()
}
let data = resp.bytes().await?.to_vec();
let value: T = serde_json::from_slice(&data)?;
Ok((value, response))
}
}
fn status_code_to_err(status: u16, body: &[u8]) -> crate::Result<()> {
if status / 100 == 2 {
return Ok(());
}
if let Ok(err_map) = serde_json::from_slice::<serde_json::Value>(body)
&& let Some(message) = err_map.get("message").and_then(|v| v.as_str())
{
return Err(crate::Error::Api {
status,
message: message.to_string(),
body: body.to_vec(),
});
}
Err(crate::Error::UnknownApi {
status,
body: String::from_utf8_lossy(body).to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_code_to_err_success() {
assert!(status_code_to_err(200, b"").is_ok());
assert!(status_code_to_err(201, b"created").is_ok());
assert!(status_code_to_err(299, b"").is_ok());
}
#[test]
fn test_status_code_to_err_api_error() {
let body = br#"{"message":"Not Found"}"#;
let err = status_code_to_err(404, body).unwrap_err();
match err {
crate::Error::Api {
status,
message,
body: err_body,
} => {
assert_eq!(status, 404);
assert_eq!(message, "Not Found");
assert_eq!(err_body, body.as_slice());
}
other => panic!("expected Error::Api, got: {other}"),
}
}
#[test]
fn test_status_code_to_err_unknown_api() {
let body = b"Internal Server Error";
let err = status_code_to_err(500, body).unwrap_err();
match err {
crate::Error::UnknownApi {
status,
body: err_body,
} => {
assert_eq!(status, 500);
assert_eq!(err_body, "Internal Server Error");
}
other => panic!("expected Error::UnknownApi, got: {other}"),
}
}
#[test]
fn test_status_code_to_err_json_no_message() {
let body = br#"{"error":"bad request"}"#;
let err = status_code_to_err(400, body).unwrap_err();
match err {
crate::Error::UnknownApi {
status,
body: err_body,
} => {
assert_eq!(status, 400);
assert_eq!(err_body, r#"{"error":"bad request"}"#);
}
other => panic!("expected Error::UnknownApi, got: {other}"),
}
}
#[test]
fn test_status_code_to_err_empty_body() {
let body = b"";
let err = status_code_to_err(500, body).unwrap_err();
match err {
crate::Error::UnknownApi {
status,
body: err_body,
} => {
assert_eq!(status, 500);
assert!(err_body.is_empty());
}
other => panic!("expected Error::UnknownApi, got: {other}"),
}
}
#[test]
fn test_status_code_to_err_array_body() {
let body = b"[]";
let err = status_code_to_err(500, body).unwrap_err();
match err {
crate::Error::UnknownApi { status, .. } => {
assert_eq!(status, 500);
}
other => panic!("expected Error::UnknownApi, got: {other}"),
}
}
#[test]
fn test_status_code_to_err_message_with_number() {
let body = br#"{"message":42}"#;
let err = status_code_to_err(422, body).unwrap_err();
assert!(
matches!(err, crate::Error::UnknownApi { .. }),
"expected Error::UnknownApi when message is not a string, got: {err}"
);
}
#[tokio::test]
async fn test_do_request_raw_signs_when_ssh_signer_present() {
use wiremock::matchers::{header_exists, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/version"))
.and(header_exists("Signature"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"version": "1.22.0"})),
)
.mount(&server)
.await;
let tmp = std::env::temp_dir().join("gitea_sdk_test_ssh_wiremock_sign");
std::fs::write(
&tmp,
include_bytes!("../../tests/ssh_fixtures/id_ed25519_test"),
)
.expect("write temp key");
let client = crate::Client::builder(&server.uri())
.ssh_cert("test-principal", &tmp, None::<&str>)
.expect("ssh_cert should succeed")
.build()
.expect("build should succeed");
let (version, _resp) = client
.miscellaneous()
.get_version()
.await
.expect("get_version should succeed");
assert_eq!(version, "1.22.0");
let _ = std::fs::remove_file(&tmp);
}
#[tokio::test]
async fn test_do_request_raw_no_signature_when_no_ssh_signer() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/version"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"version": "1.22.0"})),
)
.mount(&server)
.await;
let client = crate::Client::builder(&server.uri())
.build()
.expect("build should succeed");
let (version, _resp) = client
.miscellaneous()
.get_version()
.await
.expect("get_version should succeed");
assert_eq!(version, "1.22.0");
}
}