use gitlab::{AsyncGitlab, GitlabBuilder};
use ironflow_core::error::OperationError;
use ironflow_core::operation::OperationContext;
use crate::operation::GitLabOp;
pub struct GitLab {
inner: AsyncGitlab,
}
impl GitLab {
pub async fn from_context(ctx: &OperationContext) -> Result<Self, OperationError> {
Self::from_context_with_host(ctx, "gitlab.com").await
}
pub async fn from_context_with_host(
ctx: &OperationContext,
host: &str,
) -> Result<Self, OperationError> {
let secret =
ctx.secrets()
.get("gitlab_token")
.await?
.ok_or_else(|| OperationError::Secret {
message: "gitlab_token secret not found".to_string(),
})?;
Self::new(&secret.value, host).await
}
pub async fn new(token: &str, host: &str) -> Result<Self, OperationError> {
let inner = GitlabBuilder::new(host, token)
.build_async()
.await
.map_err(|e| OperationError::Http {
status: None,
message: e.to_string(),
})?;
Ok(Self { inner })
}
pub fn client(&self) -> &AsyncGitlab {
&self.inner
}
pub fn op<E>(&self, endpoint: E) -> GitLabOp<E> {
GitLabOp::new(self.inner.clone(), endpoint)
}
}
impl From<AsyncGitlab> for GitLab {
fn from(inner: AsyncGitlab) -> Self {
Self { inner }
}
}
impl std::fmt::Debug for GitLab {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GitLab")
.field("client", &"[AsyncGitlab]")
.finish()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use gitlab::GitlabBuilder;
use gitlab::api::projects;
use ironflow_core::operation::{NoopSecretResolver, Operation, OperationContext};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
#[tokio::test]
#[ignore]
async fn new_builds_with_valid_host() {
let gitlab = GitLab::new("glpat-xxxx", "gitlab.com").await;
assert!(gitlab.is_ok());
}
#[tokio::test]
#[ignore]
async fn debug_does_not_leak_token() {
let gitlab = GitLab::new("super-secret", "gitlab.com").await.unwrap();
let debug = format!("{gitlab:?}");
assert!(!debug.contains("super-secret"));
}
#[tokio::test]
async fn from_context_fails_when_token_missing() {
let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
let err = GitLab::from_context(&ctx).await.unwrap_err();
assert!(err.to_string().contains("gitlab_token"));
}
#[tokio::test]
async fn from_async_gitlab_hits_mock_server() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v4/user"))
.respond_with(
ResponseTemplate::new(200).set_body_string(r#"{"id":1,"username":"demo"}"#),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/api/v4/projects/42"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"id":42,"name":"demo"}"#))
.mount(&server)
.await;
let client = GitlabBuilder::new(server.address().to_string(), "test-token")
.insecure()
.build_async()
.await
.unwrap();
let gitlab: GitLab = client.into();
let endpoint = projects::Project::builder().project(42).build().unwrap();
let op = gitlab.op(endpoint);
let ctx = OperationContext::new(Arc::new(NoopSecretResolver));
let result = op.execute(&ctx).await.unwrap();
assert_eq!(result["id"], 42);
assert_eq!(result["name"], "demo");
let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 2);
let paths: Vec<&str> = requests.iter().map(|r| r.url.path()).collect();
assert!(paths.contains(&"/api/v4/user"));
assert!(paths.contains(&"/api/v4/projects/42"));
}
}