From 7814b2a66fe939966e763148245971baec9ccf24 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 3 Nov 2025 15:55:20 +0100 Subject: [PATCH] trustpub: Implement `GET /api/v1/trusted_publishing/gitlab_configs` API endpoint --- .../trustpub/gitlab_configs/json.rs | 14 + .../trustpub/gitlab_configs/list.rs | 181 ++++++++++ .../trustpub/gitlab_configs/mod.rs | 1 + src/router.rs | 1 + .../routes/trustpub/gitlab_configs/list.rs | 322 ++++++++++++++++++ .../routes/trustpub/gitlab_configs/mod.rs | 1 + ...onfigs__list__crate_with_no_configs-2.snap | 11 + ...b__gitlab_configs__list__happy_path-2.snap | 32 ++ ...b__gitlab_configs__list__happy_path-4.snap | 22 ++ ...ab_configs__list__legacy_token_auth-2.snap | 22 ++ ...b__gitlab_configs__list__pagination-2.snap | 62 ++++ ...b__gitlab_configs__list__pagination-4.snap | 62 ++++ ...b__gitlab_configs__list__pagination-6.snap | 62 ++++ ...b__gitlab_configs__list__pagination-8.snap | 11 + ..._auth_with_trusted_publishing_scope-2.snap | 22 ++ ...oken_auth_with_wildcard_crate_scope-2.snap | 22 ++ ...egration__openapi__openapi_snapshot-2.snap | 103 ++++++ 17 files changed, 951 insertions(+) create mode 100644 src/controllers/trustpub/gitlab_configs/list.rs create mode 100644 src/tests/routes/trustpub/gitlab_configs/list.rs create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__crate_with_no_configs-2.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-2.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-4.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__legacy_token_auth-2.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-2.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-4.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-6.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-8.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_trusted_publishing_scope-2.snap create mode 100644 src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_wildcard_crate_scope-2.snap diff --git a/src/controllers/trustpub/gitlab_configs/json.rs b/src/controllers/trustpub/gitlab_configs/json.rs index 239506f0a4e..49818f5b9d6 100644 --- a/src/controllers/trustpub/gitlab_configs/json.rs +++ b/src/controllers/trustpub/gitlab_configs/json.rs @@ -18,4 +18,18 @@ pub struct CreateResponse { #[derive(Debug, Serialize, utoipa::ToSchema)] pub struct ListResponse { pub gitlab_configs: Vec, + + #[schema(inline)] + pub meta: ListResponseMeta, +} + +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct ListResponseMeta { + /// The total number of GitLab configs belonging to the crate. + #[schema(example = 42)] + pub total: i64, + + /// Query string to the next page of results, if any. + #[schema(example = "?seek=abc123")] + pub next_page: Option, } diff --git a/src/controllers/trustpub/gitlab_configs/list.rs b/src/controllers/trustpub/gitlab_configs/list.rs new file mode 100644 index 00000000000..2820d662012 --- /dev/null +++ b/src/controllers/trustpub/gitlab_configs/list.rs @@ -0,0 +1,181 @@ +use crate::app::AppState; +use crate::auth::AuthCheck; +use crate::controllers::helpers::pagination::{ + Page, PaginationOptions, PaginationQueryParams, encode_seek, +}; +use crate::controllers::krate::load_crate; +use crate::controllers::trustpub::gitlab_configs::json::{self, ListResponse, ListResponseMeta}; +use crate::util::RequestUtils; +use crate::util::errors::{AppResult, bad_request}; +use axum::Json; +use axum::extract::{FromRequestParts, Query}; +use crates_io_database::models::OwnerKind; +use crates_io_database::models::token::EndpointScope; +use crates_io_database::models::trustpub::GitLabConfig; +use crates_io_database::schema::{crate_owners, trustpub_configs_gitlab}; +use diesel::dsl::{exists, select}; +use diesel::prelude::*; +use diesel_async::RunQueryDsl; +use http::request::Parts; +use indexmap::IndexMap; +use serde::Deserialize; + +#[derive(Debug, Deserialize, FromRequestParts, utoipa::IntoParams)] +#[from_request(via(Query))] +#[into_params(parameter_in = Query)] +pub struct ListQueryParams { + /// Name of the crate to list Trusted Publishing configurations for. + #[serde(rename = "crate")] + pub krate: String, +} + +/// List Trusted Publishing configurations for GitLab CI/CD. +#[utoipa::path( + get, + path = "/api/v1/trusted_publishing/gitlab_configs", + params(ListQueryParams, PaginationQueryParams), + security(("cookie" = []), ("api_token" = [])), + tag = "trusted_publishing", + responses((status = 200, description = "Successful Response", body = inline(ListResponse))), +)] +pub async fn list_trustpub_gitlab_configs( + state: AppState, + params: ListQueryParams, + parts: Parts, +) -> AppResult> { + let mut conn = state.db_read().await?; + + let auth = AuthCheck::default() + .with_endpoint_scope(EndpointScope::TrustedPublishing) + .for_crate(¶ms.krate) + .check(&parts, &mut conn) + .await?; + let auth_user = auth.user(); + + let krate = load_crate(&mut conn, ¶ms.krate).await?; + + // Check if the authenticated user is an owner of the crate + let is_owner = select(exists( + crate_owners::table + .filter(crate_owners::crate_id.eq(krate.id)) + .filter(crate_owners::deleted.eq(false)) + .filter(crate_owners::owner_kind.eq(OwnerKind::User)) + .filter(crate_owners::owner_id.eq(auth_user.id)), + )) + .get_result::(&mut conn) + .await?; + + if !is_owner { + return Err(bad_request("You are not an owner of this crate")); + } + + let pagination = PaginationOptions::builder() + .enable_seek(true) + .enable_pages(false) + .gather(&parts)?; + + let (configs, total, next_page) = + list_configs(&mut conn, krate.id, &pagination, &parts).await?; + + let gitlab_configs = configs + .into_iter() + .map(|config| json::GitLabConfig { + id: config.id, + krate: krate.name.clone(), + namespace: config.namespace, + namespace_id: config.namespace_id, + project: config.project, + workflow_filepath: config.workflow_filepath, + environment: config.environment, + created_at: config.created_at, + }) + .collect(); + + Ok(Json(ListResponse { + gitlab_configs, + meta: ListResponseMeta { total, next_page }, + })) +} + +async fn list_configs( + conn: &mut diesel_async::AsyncPgConnection, + crate_id: i32, + options: &PaginationOptions, + req: &Parts, +) -> AppResult<(Vec, i64, Option)> { + use seek::*; + + let seek = Seek::Id; + + assert!( + !matches!(&options.page, Page::Numeric(_)), + "?page= is not supported" + ); + + let make_base_query = || { + GitLabConfig::query() + .filter(trustpub_configs_gitlab::crate_id.eq(crate_id)) + .into_boxed() + }; + + let mut query = make_base_query(); + query = query.limit(options.per_page); + query = query.order(trustpub_configs_gitlab::id.asc()); + + if let Some(SeekPayload::Id(Id { id })) = seek.after(&options.page)? { + query = query.filter(trustpub_configs_gitlab::id.gt(id)); + } + + let data: Vec = query.load(conn).await?; + + let next_page = next_seek_params(&data, options, |last| seek.to_payload(last))? + .map(|p| req.query_with_params(p)); + + // Avoid the count query if we're on the first page and got fewer results than requested + let total = + if matches!(options.page, Page::Unspecified) && data.len() < options.per_page as usize { + data.len() as i64 + } else { + make_base_query().count().get_result(conn).await? + }; + + Ok((data, total, next_page)) +} + +fn next_seek_params( + records: &[T], + options: &PaginationOptions, + f: F, +) -> AppResult>> +where + F: Fn(&T) -> S, + S: serde::Serialize, +{ + if records.len() < options.per_page as usize { + return Ok(None); + } + + let seek = f(records.last().unwrap()); + let mut opts = IndexMap::new(); + opts.insert("seek".into(), encode_seek(seek)?); + Ok(Some(opts)) +} + +mod seek { + use crate::controllers::helpers::pagination::seek; + use crates_io_database::models::trustpub::GitLabConfig; + + seek!( + pub enum Seek { + Id { id: i32 }, + } + ); + + impl Seek { + pub(crate) fn to_payload(&self, record: &GitLabConfig) -> SeekPayload { + match *self { + Seek::Id => SeekPayload::Id(Id { id: record.id }), + } + } + } +} diff --git a/src/controllers/trustpub/gitlab_configs/mod.rs b/src/controllers/trustpub/gitlab_configs/mod.rs index 3621f02d902..5b6bc46270a 100644 --- a/src/controllers/trustpub/gitlab_configs/mod.rs +++ b/src/controllers/trustpub/gitlab_configs/mod.rs @@ -1,3 +1,4 @@ pub mod create; pub mod delete; pub mod json; +pub mod list; diff --git a/src/router.rs b/src/router.rs index 6b7f5813bf0..73cddeab45b 100644 --- a/src/router.rs +++ b/src/router.rs @@ -101,6 +101,7 @@ pub fn build_axum_router(state: AppState) -> Router<()> { .routes(routes!( trustpub::gitlab_configs::create::create_trustpub_gitlab_config, trustpub::gitlab_configs::delete::delete_trustpub_gitlab_config, + trustpub::gitlab_configs::list::list_trustpub_gitlab_configs, )) .split_for_parts(); diff --git a/src/tests/routes/trustpub/gitlab_configs/list.rs b/src/tests/routes/trustpub/gitlab_configs/list.rs new file mode 100644 index 00000000000..98cbf81905b --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/list.rs @@ -0,0 +1,322 @@ +use crate::builders::CrateBuilder; +use crate::util::{RequestHelper, TestApp}; +use crates_io_database::models::token::{CrateScope, EndpointScope}; +use crates_io_database::models::trustpub::{GitLabConfig, NewGitLabConfig}; +use diesel::prelude::*; +use diesel_async::AsyncPgConnection; +use insta::{assert_json_snapshot, assert_snapshot}; +use serde_json::json; + +const URL: &str = "/api/v1/trusted_publishing/gitlab_configs"; + +async fn create_config( + conn: &mut AsyncPgConnection, + crate_id: i32, + project: &str, +) -> QueryResult { + let config = NewGitLabConfig { + crate_id, + namespace: "rust-lang", + project, + workflow_filepath: ".gitlab-ci.yml", + environment: None, + }; + + config.insert(conn).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_happy_path() -> anyhow::Result<()> { + let (app, _client, cookie_client) = TestApp::full().with_user().await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let foo = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + let bar = CrateBuilder::new("bar", owner_id).build(&mut conn).await?; + + create_config(&mut conn, foo.id, "foo-rs").await?; + create_config(&mut conn, foo.id, "foo").await?; + create_config(&mut conn, bar.id, "BAR").await?; + + let response = cookie_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"200 OK"); + assert_json_snapshot!(response.json(), { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + let response = cookie_client.get_with_query::<()>(URL, "crate=Bar").await; + assert_snapshot!(response.status(), @"200 OK"); + assert_json_snapshot!(response.json(), { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_unauthorized() -> anyhow::Result<()> { + let (app, anon_client, cookie_client) = TestApp::full().with_user().await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let response = anon_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"403 Forbidden"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"this action requires authentication"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_not_owner() -> anyhow::Result<()> { + let (app, _, cookie_client) = TestApp::full().with_user().await; + let mut conn = app.db_conn().await; + + // Create a different user who will be the owner of the crate + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + // The authenticated user is not an owner of the crate + let other_user = app.db_new_user("other").await; + let response = other_user.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"400 Bad Request"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"You are not an owner of this crate"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_team_owner() -> anyhow::Result<()> { + let (app, _) = TestApp::full().empty().await; + let mut conn = app.db_conn().await; + + let user = app.db_new_user("user-org-owner").await; + let user2 = app.db_new_user("user-one-team").await; + + let owner_id = user.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let body = json!({ "owners": ["github:test-org:all"] }).to_string(); + let response = user.put::<()>("/api/v1/crates/foo/owners", body).await; + assert_snapshot!(response.status(), @"200 OK"); + + let response = user2.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"400 Bad Request"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"You are not an owner of this crate"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_crate_not_found() -> anyhow::Result<()> { + let (_, _, cookie_client) = TestApp::full().with_user().await; + + let response = cookie_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"404 Not Found"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"crate `foo` does not exist"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_no_query_param() -> anyhow::Result<()> { + let (_, _, cookie_client) = TestApp::full().with_user().await; + + let response = cookie_client.get::<()>(URL).await; + assert_snapshot!(response.status(), @"400 Bad Request"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"Failed to deserialize query string: missing field `crate`"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_crate_with_no_configs() -> anyhow::Result<()> { + let (app, _, cookie_client) = TestApp::full().with_user().await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + + // No configs have been created for this crate + let response = cookie_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"200 OK"); + assert_json_snapshot!(response.json(), { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_legacy_token_auth() -> anyhow::Result<()> { + let (app, _client, cookie_client, token_client) = TestApp::full().with_token().await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let response = token_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"200 OK"); + assert_json_snapshot!(response.json(), { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_token_auth_with_trusted_publishing_scope() -> anyhow::Result<()> { + let (app, _client, cookie_client, token_client) = TestApp::full() + .with_scoped_token( + Some(vec![CrateScope::try_from("foo").unwrap()]), + Some(vec![EndpointScope::TrustedPublishing]), + ) + .await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let response = token_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"200 OK"); + assert_json_snapshot!(response.json(), { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_token_auth_without_trusted_publishing_scope() -> anyhow::Result<()> { + let (app, _client, cookie_client, token_client) = TestApp::full() + .with_scoped_token( + Some(vec![CrateScope::try_from("foo").unwrap()]), + Some(vec![EndpointScope::PublishUpdate]), + ) + .await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let response = token_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"403 Forbidden"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"this token does not have the required permissions to perform this action"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_token_auth_with_wrong_crate_scope() -> anyhow::Result<()> { + let (app, _client, cookie_client, token_client) = TestApp::full() + .with_scoped_token( + Some(vec![CrateScope::try_from("other-crate").unwrap()]), + Some(vec![EndpointScope::TrustedPublishing]), + ) + .await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let response = token_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"403 Forbidden"); + assert_snapshot!(response.text(), @r#"{"errors":[{"detail":"this token does not have the required permissions to perform this action"}]}"#); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_token_auth_with_wildcard_crate_scope() -> anyhow::Result<()> { + let (app, _client, cookie_client, token_client) = TestApp::full() + .with_scoped_token( + Some(vec![CrateScope::try_from("*").unwrap()]), + Some(vec![EndpointScope::TrustedPublishing]), + ) + .await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + create_config(&mut conn, krate.id, "foo-rs").await?; + + let response = token_client.get_with_query::<()>(URL, "crate=foo").await; + assert_snapshot!(response.status(), @"200 OK"); + assert_json_snapshot!(response.json(), { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_pagination() -> anyhow::Result<()> { + let (app, _, cookie_client) = TestApp::full().with_user().await; + let mut conn = app.db_conn().await; + + let owner_id = cookie_client.as_model().id; + let krate = CrateBuilder::new("foo", owner_id).build(&mut conn).await?; + + // Create 15 configs + for i in 0..15 { + create_config(&mut conn, krate.id, &format!("repo-{i}")).await?; + } + + // Request first page with per_page=5 + let response = cookie_client + .get_with_query::<()>(URL, "crate=foo&per_page=5") + .await; + assert_snapshot!(response.status(), @"200 OK"); + let json = response.json(); + assert_json_snapshot!(json, { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + // Extract the next_page URL and make a second request + let next_page = json["meta"]["next_page"] + .as_str() + .expect("next_page should be present"); + let next_url = format!("{}{}", URL, next_page); + let response = cookie_client.get::<()>(&next_url).await; + assert_snapshot!(response.status(), @"200 OK"); + let json = response.json(); + assert_json_snapshot!(json, { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + // Third page (last page with data) + let next_page = json["meta"]["next_page"] + .as_str() + .expect("next_page should be present"); + let next_url = format!("{}{}", URL, next_page); + let response = cookie_client.get::<()>(&next_url).await; + assert_snapshot!(response.status(), @"200 OK"); + let json = response.json(); + assert_json_snapshot!(json, { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + // The third page has exactly 5 items, so next_page will be present + // (cursor-based pagination is conservative about indicating more pages) + // Following it should give us an empty fourth page + let next_page = json["meta"]["next_page"] + .as_str() + .expect("next_page should be present on third page"); + let next_url = format!("{}{}", URL, next_page); + let response = cookie_client.get::<()>(&next_url).await; + assert_snapshot!(response.status(), @"200 OK"); + let json = response.json(); + assert_json_snapshot!(json, { + ".gitlab_configs[].created_at" => "[datetime]", + }); + + Ok(()) +} diff --git a/src/tests/routes/trustpub/gitlab_configs/mod.rs b/src/tests/routes/trustpub/gitlab_configs/mod.rs index aeaae9dddf0..bc76d87cac4 100644 --- a/src/tests/routes/trustpub/gitlab_configs/mod.rs +++ b/src/tests/routes/trustpub/gitlab_configs/mod.rs @@ -1,2 +1,3 @@ mod create; mod delete; +mod list; diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__crate_with_no_configs-2.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__crate_with_no_configs-2.snap new file mode 100644 index 00000000000..e26791956a0 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__crate_with_no_configs-2.snap @@ -0,0 +1,11 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: response.json() +--- +{ + "gitlab_configs": [], + "meta": { + "next_page": null, + "total": 0 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-2.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-2.snap new file mode 100644 index 00000000000..17ce7c9bd9d --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-2.snap @@ -0,0 +1,32 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: response.json() +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 1, + "namespace": "rust-lang", + "namespace_id": null, + "project": "foo-rs", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 2, + "namespace": "rust-lang", + "namespace_id": null, + "project": "foo", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": null, + "total": 2 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-4.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-4.snap new file mode 100644 index 00000000000..f2e502efb9e --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__happy_path-4.snap @@ -0,0 +1,22 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: response.json() +--- +{ + "gitlab_configs": [ + { + "crate": "bar", + "created_at": "[datetime]", + "environment": null, + "id": 3, + "namespace": "rust-lang", + "namespace_id": null, + "project": "BAR", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": null, + "total": 1 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__legacy_token_auth-2.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__legacy_token_auth-2.snap new file mode 100644 index 00000000000..21c78814e39 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__legacy_token_auth-2.snap @@ -0,0 +1,22 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: response.json() +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 1, + "namespace": "rust-lang", + "namespace_id": null, + "project": "foo-rs", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": null, + "total": 1 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-2.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-2.snap new file mode 100644 index 00000000000..4a964f11cd6 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-2.snap @@ -0,0 +1,62 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: json +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 1, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-0", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 2, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-1", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 3, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-2", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 4, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-3", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 5, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-4", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": "?crate=foo&per_page=5&seek=NQ", + "total": 15 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-4.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-4.snap new file mode 100644 index 00000000000..552bad7c551 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-4.snap @@ -0,0 +1,62 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: json +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 6, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-5", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 7, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-6", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 8, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-7", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 9, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-8", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 10, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-9", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": "?crate=foo&per_page=5&seek=MTA", + "total": 15 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-6.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-6.snap new file mode 100644 index 00000000000..29c2aecd95a --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-6.snap @@ -0,0 +1,62 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: json +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 11, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-10", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 12, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-11", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 13, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-12", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 14, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-13", + "workflow_filepath": ".gitlab-ci.yml" + }, + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 15, + "namespace": "rust-lang", + "namespace_id": null, + "project": "repo-14", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": "?crate=foo&per_page=5&seek=MTU", + "total": 15 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-8.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-8.snap new file mode 100644 index 00000000000..6e707905f35 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__pagination-8.snap @@ -0,0 +1,11 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: json +--- +{ + "gitlab_configs": [], + "meta": { + "next_page": null, + "total": 15 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_trusted_publishing_scope-2.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_trusted_publishing_scope-2.snap new file mode 100644 index 00000000000..21c78814e39 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_trusted_publishing_scope-2.snap @@ -0,0 +1,22 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: response.json() +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 1, + "namespace": "rust-lang", + "namespace_id": null, + "project": "foo-rs", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": null, + "total": 1 + } +} diff --git a/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_wildcard_crate_scope-2.snap b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_wildcard_crate_scope-2.snap new file mode 100644 index 00000000000..21c78814e39 --- /dev/null +++ b/src/tests/routes/trustpub/gitlab_configs/snapshots/integration__routes__trustpub__gitlab_configs__list__token_auth_with_wildcard_crate_scope-2.snap @@ -0,0 +1,22 @@ +--- +source: src/tests/routes/trustpub/gitlab_configs/list.rs +expression: response.json() +--- +{ + "gitlab_configs": [ + { + "crate": "foo", + "created_at": "[datetime]", + "environment": null, + "id": 1, + "namespace": "rust-lang", + "namespace_id": null, + "project": "foo-rs", + "workflow_filepath": ".gitlab-ci.yml" + } + ], + "meta": { + "next_page": null, + "total": 1 + } +} diff --git a/src/tests/snapshots/integration__openapi__openapi_snapshot-2.snap b/src/tests/snapshots/integration__openapi__openapi_snapshot-2.snap index 94b8a1bd1a4..be723e9e571 100644 --- a/src/tests/snapshots/integration__openapi__openapi_snapshot-2.snap +++ b/src/tests/snapshots/integration__openapi__openapi_snapshot-2.snap @@ -4479,6 +4479,109 @@ expression: response.json() } }, "/api/v1/trusted_publishing/gitlab_configs": { + "get": { + "operationId": "list_trustpub_gitlab_configs", + "parameters": [ + { + "description": "Name of the crate to list Trusted Publishing configurations for.", + "in": "query", + "name": "crate", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The page number to request.\n\nThis parameter is mutually exclusive with `seek` and not supported for\nall requests.", + "in": "query", + "name": "page", + "required": false, + "schema": { + "format": "int32", + "minimum": 1, + "type": "integer" + } + }, + { + "description": "The number of items to request per page.", + "in": "query", + "name": "per_page", + "required": false, + "schema": { + "format": "int32", + "minimum": 1, + "type": "integer" + } + }, + { + "description": "The seek key to request.\n\nThis parameter is mutually exclusive with `page` and not supported for\nall requests.\n\nThe seek key can usually be found in the `meta.next_page` field of\npaginated responses.", + "in": "query", + "name": "seek", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "gitlab_configs": { + "items": { + "$ref": "#/components/schemas/GitLabConfig" + }, + "type": "array" + }, + "meta": { + "properties": { + "next_page": { + "description": "Query string to the next page of results, if any.", + "example": "?seek=abc123", + "type": [ + "string", + "null" + ] + }, + "total": { + "description": "The total number of GitLab configs belonging to the crate.", + "example": 42, + "format": "int64", + "type": "integer" + } + }, + "required": [ + "total" + ], + "type": "object" + } + }, + "required": [ + "gitlab_configs", + "meta" + ], + "type": "object" + } + } + }, + "description": "Successful Response" + } + }, + "security": [ + { + "cookie": [] + }, + { + "api_token": [] + } + ], + "summary": "List Trusted Publishing configurations for GitLab CI/CD.", + "tags": [ + "trusted_publishing" + ] + }, "post": { "operationId": "create_trustpub_gitlab_config", "requestBody": {