gitlab 0.1900.1

Gitlab API client.
Documentation
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use derive_builder::Builder;
use serde_json::json;

use crate::api::common::{NameOrId, SecretStr};
use crate::api::endpoint_prelude::*;

/// Import a project from an exported archive stored in AWS S3.
#[derive(Debug, Builder, Clone)]
#[builder(derive(Debug), setter(strip_option))]
pub struct RemoteImportS3<'a> {
    /// AWS S3 access key ID.
    #[builder(setter(into))]
    access_key_id: Cow<'a, str>,

    /// AWS S3 bucket name where the exported project archive is stored.
    #[builder(setter(into))]
    bucket_name: Cow<'a, str>,

    /// Key (path) of the exported project archive within the S3 bucket.
    #[builder(setter(into))]
    file_key: Cow<'a, str>,

    /// The path for the new project, including namespace (`namespace/project-name`).
    #[builder(setter(into))]
    path: Cow<'a, str>,

    /// AWS region where the S3 bucket is hosted.
    #[builder(setter(into))]
    region: Cow<'a, str>,

    /// AWS S3 secret access key.
    #[builder(setter(into))]
    secret_access_key: SecretStr<'a>,

    /// Name of the project to import. Defaults to the path of the project if not provided.
    #[builder(setter(into), default)]
    name: Option<Cow<'a, str>>,

    /// Namespace to import the project into. Defaults to the current user's namespace.
    #[builder(setter(into), default)]
    namespace: Option<NameOrId<'a>>,
}

impl<'a> RemoteImportS3<'a> {
    /// Create a builder for the endpoint.
    pub fn builder() -> RemoteImportS3Builder<'a> {
        RemoteImportS3Builder::default()
    }

    fn as_json(&self) -> serde_json::Value {
        let (namespace_id, namespace_path) = match &self.namespace {
            Some(NameOrId::Id(id)) => (Some(*id), None),
            Some(NameOrId::Name(path)) => (None, Some(path.as_ref())),
            None => (None, None),
        };
        JsonParams::clean(json!({
            "access_key_id": self.access_key_id,
            "bucket_name": self.bucket_name,
            "file_key": self.file_key,
            "name": self.name,
            "namespace_id": namespace_id,
            "namespace_path": namespace_path,
            "path": self.path,
            "region": self.region,
            "secret_access_key": self.secret_access_key,
        }))
    }
}

impl Endpoint for RemoteImportS3<'_> {
    fn method(&self) -> Method {
        Method::POST
    }

    fn endpoint(&self) -> Cow<'static, str> {
        "projects/remote-import-s3".into()
    }

    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
        JsonParams::into_body(&self.as_json())
    }
}

#[cfg(test)]
mod tests {
    use http::Method;

    use crate::api::projects::import::{RemoteImportS3, RemoteImportS3BuilderError};
    use crate::api::{self, Query};
    use crate::test::client::{ExpectedUrl, SingleTestClient};

    fn required_endpoint(body: &str) -> ExpectedUrl {
        ExpectedUrl::builder()
            .method(Method::POST)
            .endpoint("projects/remote-import-s3")
            .content_type("application/json")
            .body_str(body)
            .build()
            .unwrap()
    }

    fn required_builder() -> crate::api::projects::import::RemoteImportS3Builder<'static> {
        let mut b = RemoteImportS3::builder();
        b.access_key_id("AKIAIOSFODNN7EXAMPLE")
            .bucket_name("my-bucket")
            .file_key("export.tar.gz")
            .path("my-group/my-project")
            .region("us-east-1")
            .secret_access_key("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
        b
    }

    #[test]
    fn debug_redacts_secret_access_key() {
        let endpoint = required_builder().build().unwrap();
        let debug = format!("{endpoint:?}");
        assert!(
            !debug.contains("wJalrXUtnFEMI"),
            "secret_access_key must not appear in Debug output",
        );
        assert!(
            debug.contains("[REDACTED]"),
            "Debug output must contain redaction marker",
        );
        assert!(
            debug.contains("AKIAIOSFODNN7EXAMPLE"),
            "access_key_id should remain visible",
        );
    }

    #[test]
    fn builder_debug_redacts_secret_access_key() {
        let debug = format!("{:?}", required_builder());
        assert!(
            !debug.contains("wJalrXUtnFEMI"),
            "secret_access_key must not appear in builder Debug output"
        );
        assert!(
            debug.contains("[REDACTED]"),
            "builder Debug output must contain redaction marker"
        );
        assert!(
            debug.contains("AKIAIOSFODNN7EXAMPLE"),
            "access_key_id should remain visible in builder Debug output"
        );
    }

    #[test]
    fn access_key_id_is_necessary() {
        let err = RemoteImportS3::builder()
            .bucket_name("my-bucket")
            .file_key("export.tar.gz")
            .path("my-group/my-project")
            .region("us-east-1")
            .secret_access_key("secret")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, RemoteImportS3BuilderError, "access_key_id");
    }

    #[test]
    fn bucket_name_is_necessary() {
        let err = RemoteImportS3::builder()
            .access_key_id("key")
            .file_key("export.tar.gz")
            .path("my-group/my-project")
            .region("us-east-1")
            .secret_access_key("secret")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, RemoteImportS3BuilderError, "bucket_name");
    }

    #[test]
    fn file_key_is_necessary() {
        let err = RemoteImportS3::builder()
            .access_key_id("key")
            .bucket_name("my-bucket")
            .path("my-group/my-project")
            .region("us-east-1")
            .secret_access_key("secret")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, RemoteImportS3BuilderError, "file_key");
    }

    #[test]
    fn path_is_necessary() {
        let err = RemoteImportS3::builder()
            .access_key_id("key")
            .bucket_name("my-bucket")
            .file_key("export.tar.gz")
            .region("us-east-1")
            .secret_access_key("secret")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, RemoteImportS3BuilderError, "path");
    }

    #[test]
    fn region_is_necessary() {
        let err = RemoteImportS3::builder()
            .access_key_id("key")
            .bucket_name("my-bucket")
            .file_key("export.tar.gz")
            .path("my-group/my-project")
            .secret_access_key("secret")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, RemoteImportS3BuilderError, "region");
    }

    #[test]
    fn secret_access_key_is_necessary() {
        let err = RemoteImportS3::builder()
            .access_key_id("key")
            .bucket_name("my-bucket")
            .file_key("export.tar.gz")
            .path("my-group/my-project")
            .region("us-east-1")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, RemoteImportS3BuilderError, "secret_access_key");
    }

    #[test]
    fn all_required_fields_are_sufficient() {
        required_builder().build().unwrap();
    }

    #[test]
    fn endpoint() {
        let endpoint = required_endpoint(concat!(
            "{",
            "\"access_key_id\":\"AKIAIOSFODNN7EXAMPLE\",",
            "\"bucket_name\":\"my-bucket\",",
            "\"file_key\":\"export.tar.gz\",",
            "\"path\":\"my-group/my-project\",",
            "\"region\":\"us-east-1\",",
            "\"secret_access_key\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"",
            "}",
        ));
        let client = SingleTestClient::new_raw(endpoint, "");
        api::ignore(required_builder().build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_name() {
        let endpoint = required_endpoint(concat!(
            "{",
            "\"access_key_id\":\"AKIAIOSFODNN7EXAMPLE\",",
            "\"bucket_name\":\"my-bucket\",",
            "\"file_key\":\"export.tar.gz\",",
            "\"name\":\"My Project\",",
            "\"path\":\"my-group/my-project\",",
            "\"region\":\"us-east-1\",",
            "\"secret_access_key\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"",
            "}",
        ));
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(required_builder().name("My Project").build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_namespace_id() {
        let endpoint = required_endpoint(concat!(
            "{",
            "\"access_key_id\":\"AKIAIOSFODNN7EXAMPLE\",",
            "\"bucket_name\":\"my-bucket\",",
            "\"file_key\":\"export.tar.gz\",",
            "\"namespace_id\":5,",
            "\"path\":\"my-group/my-project\",",
            "\"region\":\"us-east-1\",",
            "\"secret_access_key\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"",
            "}",
        ));
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(required_builder().namespace(5).build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_namespace_path() {
        let endpoint = required_endpoint(concat!(
            "{",
            "\"access_key_id\":\"AKIAIOSFODNN7EXAMPLE\",",
            "\"bucket_name\":\"my-bucket\",",
            "\"file_key\":\"export.tar.gz\",",
            "\"namespace_path\":\"my-group\",",
            "\"path\":\"my-group/my-project\",",
            "\"region\":\"us-east-1\",",
            "\"secret_access_key\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"",
            "}",
        ));
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(required_builder().namespace("my-group").build().unwrap())
            .query(&client)
            .unwrap();
    }
}