From 86058d5a7f8694dd931bf4e55ac0636ad830fb97 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 24 Nov 2025 07:32:52 +1300 Subject: [PATCH 01/13] feat(integrations): Integrate new attestations API In this commit, I've added a new attestations.go file, containing two new methods: ListAttestations and DownloadAttestations. The first lists attestations for a specific project and hash, and the second retrieves the bytes of a specific attestation, for local verification. This also includes tests, automatically generated files, and other required changes. --- attestations.go | 101 +++++++++++++++++++++ attestations_test.go | 96 ++++++++++++++++++++ gitlab.go | 2 + gitlab_service_map_generated_test.go | 1 + testing/api_generated.go | 1 + testing/attestations_mock.go | 130 +++++++++++++++++++++++++++ testing/client_generated.go | 4 + 7 files changed, 335 insertions(+) create mode 100644 attestations.go create mode 100644 attestations_test.go create mode 100644 testing/attestations_mock.go diff --git a/attestations.go b/attestations.go new file mode 100644 index 00000000..cd1aeb83 --- /dev/null +++ b/attestations.go @@ -0,0 +1,101 @@ +// +// Copyright 2021, Sander van Harmelen +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package gitlab + +import ( + "bytes" + "fmt" + "net/http" + "time" +) + +type ( + AttestationsServiceInterface interface { + // ListAttestations gets a list of all attestations + // + // GitLab API docs: https://docs.gitlab.com/api/attestations + ListAttestations(pid any, opt *ListAttestationsOptions, options ...RequestOptionFunc) ([]*Attestation, *Response, error) + + // DownloadAttestation + // + // GitLab API docs: https://docs.gitlab.com/api/attestations + DownloadAttestation(pid any, opt *DownloadAttestationOptions, options ...RequestOptionFunc) ([]byte, *Response, error) + } + + // AttestationsService handles communication with the keys related methods + // of the GitLab API. + // + // GitLab API docs: https://docs.gitlab.com/api/attestations + AttestationsService struct { + client *Client + } +) + +var _ AttestationsServiceInterface = (*AttestationsService)(nil) + +type Attestation struct { + ID int `json:"id"` + IID int `json:"iid"` + ProjectID int `json:"project_id"` + BuildID int `json:"build_id"` + Status string `json:"status"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + ExpireAt *time.Time `json:"expire_at"` + PredicateKind string `json:"predicate_kind"` + PredicateType string `json:"predicate_type"` + SubjectDigest string `json:"subject_digest"` + DownloadURL string `json:"download_url"` +} + +type ListAttestationsOptions struct { + SubjectDigest string +} + +func (s *AttestationsService) ListAttestations(pid any, opt *ListAttestationsOptions, options ...RequestOptionFunc) ([]*Attestation, *Response, error) { + return do[[]*Attestation](s.client, + withMethod(http.MethodGet), + withPath("projects/%s/attestations/%s", pid, opt.SubjectDigest), + withRequestOpts(options...), + ) +} + +type DownloadAttestationOptions struct { + AttestationIID int +} + +func (s *AttestationsService) DownloadAttestation(pid any, opt *DownloadAttestationOptions, options ...RequestOptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + + url := fmt.Sprintf("projects/%s/attestations/%d/download", PathEscape(project), opt.AttestationIID) + + req, err := s.client.NewRequest(http.MethodGet, url, nil, options) + if err != nil { + return nil, nil, err + } + + var body bytes.Buffer + response, err := s.client.Do(req, &body) + if err != nil { + return nil, nil, err + } + + return body.Bytes(), response, err +} diff --git a/attestations_test.go b/attestations_test.go new file mode 100644 index 00000000..0cdc40ff --- /dev/null +++ b/attestations_test.go @@ -0,0 +1,96 @@ +// +// Copyright 2021, Sander van Harmelen +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package gitlab + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListAttestations(t *testing.T) { + t.Parallel() + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1337/attestations/76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprintf(w, `[ + { + "build_id": 1337, + "created_at": "2025-10-07T20:59:27.085Z", + "download_url": "https://gitlab.com/api/v4/projects/72356192/attestations/1/download", + "expire_at": "2027-10-07T20:59:26.967Z", + "id": 1, + "iid": 1, + "predicate_kind": "provenance", + "predicate_type": "https://slsa.dev/provenance/v1", + "project_id": 1337, + "status": "success", + "subject_digest": "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c", + "updated_at": "2025-10-07T20:59:27.085Z" + } + ]`) + }) + + listAttestationOptions := ListAttestationsOptions{ + SubjectDigest: "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c", + } + attestations, resp, err := client.Attestations.ListAttestations("1337", &listAttestationOptions) + assert.NoError(t, err) + assert.NotNil(t, resp) + + want := []*Attestation{ + { + ID: 1, + IID: 1, + BuildID: 1337, + DownloadURL: "https://gitlab.com/api/v4/projects/72356192/attestations/1/download", + CreatedAt: mustParseTime("2025-10-07T20:59:27.085Z"), + UpdatedAt: mustParseTime("2025-10-07T20:59:27.085Z"), + ExpireAt: mustParseTime("2027-10-07T20:59:26.967Z"), + PredicateKind: "provenance", + PredicateType: "https://slsa.dev/provenance/v1", + ProjectID: 1337, + Status: "success", + SubjectDigest: "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c", + }, + } + assert.Equal(t, want, attestations) +} + +func TestDownloadAttestation(t *testing.T) { + t.Parallel() + mux, client := setup(t) + + expectedOut := "expected_output" + + mux.HandleFunc("/api/v4/projects/1337/attestations/1/download", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprintf(w, expectedOut) + }) + + downloadAttestationsOptions := DownloadAttestationOptions{ + AttestationIID: 1, + } + outBytes, resp, err := client.Attestations.DownloadAttestation("1337", &downloadAttestationsOptions) + assert.NoError(t, err) + assert.NotNil(t, resp) + + assert.Equal(t, []byte(expectedOut), outBytes) +} diff --git a/gitlab.go b/gitlab.go index d4b4d157..2030463e 100644 --- a/gitlab.go +++ b/gitlab.go @@ -123,6 +123,7 @@ type Client struct { Appearance AppearanceServiceInterface Applications ApplicationsServiceInterface ApplicationStatistics ApplicationStatisticsServiceInterface + Attestations AttestationsServiceInterface AuditEvents AuditEventsServiceInterface Avatar AvatarRequestsServiceInterface AwardEmoji AwardEmojiServiceInterface @@ -436,6 +437,7 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e c.Appearance = &AppearanceService{client: c} c.Applications = &ApplicationsService{client: c} c.ApplicationStatistics = &ApplicationStatisticsService{client: c} + c.Attestations = &AttestationsService{client: c} c.AuditEvents = &AuditEventsService{client: c} c.Avatar = &AvatarRequestsService{client: c} c.AwardEmoji = &AwardEmojiService{client: c} diff --git a/gitlab_service_map_generated_test.go b/gitlab_service_map_generated_test.go index 6f82178a..d300a956 100644 --- a/gitlab_service_map_generated_test.go +++ b/gitlab_service_map_generated_test.go @@ -8,6 +8,7 @@ var serviceMap = map[any]any{ &AppearanceService{}: (*AppearanceServiceInterface)(nil), &ApplicationStatisticsService{}: (*ApplicationStatisticsServiceInterface)(nil), &ApplicationsService{}: (*ApplicationsServiceInterface)(nil), + &AttestationsService{}: (*AttestationsServiceInterface)(nil), &AuditEventsService{}: (*AuditEventsServiceInterface)(nil), &AvatarRequestsService{}: (*AvatarRequestsServiceInterface)(nil), &AwardEmojiService{}: (*AwardEmojiServiceInterface)(nil), diff --git a/testing/api_generated.go b/testing/api_generated.go index e41b54a5..2ae7870f 100644 --- a/testing/api_generated.go +++ b/testing/api_generated.go @@ -7,6 +7,7 @@ package testing //go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=appearance_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go AppearanceServiceInterface //go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=application_statistics_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go ApplicationStatisticsServiceInterface //go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=applications_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go ApplicationsServiceInterface +//go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=attestations_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go AttestationsServiceInterface //go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=audit_events_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go AuditEventsServiceInterface //go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=avatar_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go AvatarRequestsServiceInterface //go:generate go run go.uber.org/mock/mockgen@v0.6.0 -typed -destination=award_emojis_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go AwardEmojiServiceInterface diff --git a/testing/attestations_mock.go b/testing/attestations_mock.go new file mode 100644 index 00000000..0ce49515 --- /dev/null +++ b/testing/attestations_mock.go @@ -0,0 +1,130 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gitlab.com/gitlab-org/api/client-go (interfaces: AttestationsServiceInterface) +// +// Generated by this command: +// +// mockgen -typed -destination=attestations_mock.go -write_package_comment=false -package=testing gitlab.com/gitlab-org/api/client-go AttestationsServiceInterface +// + +package testing + +import ( + reflect "reflect" + + gitlab "gitlab.com/gitlab-org/api/client-go" + gomock "go.uber.org/mock/gomock" +) + +// MockAttestationsServiceInterface is a mock of AttestationsServiceInterface interface. +type MockAttestationsServiceInterface struct { + ctrl *gomock.Controller + recorder *MockAttestationsServiceInterfaceMockRecorder + isgomock struct{} +} + +// MockAttestationsServiceInterfaceMockRecorder is the mock recorder for MockAttestationsServiceInterface. +type MockAttestationsServiceInterfaceMockRecorder struct { + mock *MockAttestationsServiceInterface +} + +// NewMockAttestationsServiceInterface creates a new mock instance. +func NewMockAttestationsServiceInterface(ctrl *gomock.Controller) *MockAttestationsServiceInterface { + mock := &MockAttestationsServiceInterface{ctrl: ctrl} + mock.recorder = &MockAttestationsServiceInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAttestationsServiceInterface) EXPECT() *MockAttestationsServiceInterfaceMockRecorder { + return m.recorder +} + +// DownloadAttestation mocks base method. +func (m *MockAttestationsServiceInterface) DownloadAttestation(pid any, opt *gitlab.DownloadAttestationOptions, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error) { + m.ctrl.T.Helper() + varargs := []any{pid, opt} + for _, a := range options { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DownloadAttestation", varargs...) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(*gitlab.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// DownloadAttestation indicates an expected call of DownloadAttestation. +func (mr *MockAttestationsServiceInterfaceMockRecorder) DownloadAttestation(pid, opt any, options ...any) *MockAttestationsServiceInterfaceDownloadAttestationCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pid, opt}, options...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAttestation", reflect.TypeOf((*MockAttestationsServiceInterface)(nil).DownloadAttestation), varargs...) + return &MockAttestationsServiceInterfaceDownloadAttestationCall{Call: call} +} + +// MockAttestationsServiceInterfaceDownloadAttestationCall wrap *gomock.Call +type MockAttestationsServiceInterfaceDownloadAttestationCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) Return(arg0 []byte, arg1 *gitlab.Response, arg2 error) *MockAttestationsServiceInterfaceDownloadAttestationCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) Do(f func(any, *gitlab.DownloadAttestationOptions, ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)) *MockAttestationsServiceInterfaceDownloadAttestationCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) DoAndReturn(f func(any, *gitlab.DownloadAttestationOptions, ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)) *MockAttestationsServiceInterfaceDownloadAttestationCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListAttestations mocks base method. +func (m *MockAttestationsServiceInterface) ListAttestations(pid any, opt *gitlab.ListAttestationsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error) { + m.ctrl.T.Helper() + varargs := []any{pid, opt} + for _, a := range options { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListAttestations", varargs...) + ret0, _ := ret[0].([]*gitlab.Attestation) + ret1, _ := ret[1].(*gitlab.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListAttestations indicates an expected call of ListAttestations. +func (mr *MockAttestationsServiceInterfaceMockRecorder) ListAttestations(pid, opt any, options ...any) *MockAttestationsServiceInterfaceListAttestationsCall { + mr.mock.ctrl.T.Helper() + varargs := append([]any{pid, opt}, options...) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAttestations", reflect.TypeOf((*MockAttestationsServiceInterface)(nil).ListAttestations), varargs...) + return &MockAttestationsServiceInterfaceListAttestationsCall{Call: call} +} + +// MockAttestationsServiceInterfaceListAttestationsCall wrap *gomock.Call +type MockAttestationsServiceInterfaceListAttestationsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockAttestationsServiceInterfaceListAttestationsCall) Return(arg0 []*gitlab.Attestation, arg1 *gitlab.Response, arg2 error) *MockAttestationsServiceInterfaceListAttestationsCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockAttestationsServiceInterfaceListAttestationsCall) Do(f func(any, *gitlab.ListAttestationsOptions, ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error)) *MockAttestationsServiceInterfaceListAttestationsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockAttestationsServiceInterfaceListAttestationsCall) DoAndReturn(f func(any, *gitlab.ListAttestationsOptions, ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error)) *MockAttestationsServiceInterfaceListAttestationsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/testing/client_generated.go b/testing/client_generated.go index dd14ddf3..b80793dd 100644 --- a/testing/client_generated.go +++ b/testing/client_generated.go @@ -17,6 +17,7 @@ type testClientMocks struct { MockAppearance *MockAppearanceServiceInterface MockApplications *MockApplicationsServiceInterface MockApplicationStatistics *MockApplicationStatisticsServiceInterface + MockAttestations *MockAttestationsServiceInterface MockAuditEvents *MockAuditEventsServiceInterface MockAvatar *MockAvatarRequestsServiceInterface MockAwardEmoji *MockAwardEmojiServiceInterface @@ -173,6 +174,7 @@ func newTestClientWithCtrl(ctrl *gomock.Controller, options ...gitlab.ClientOpti mockAppearance := NewMockAppearanceServiceInterface(ctrl) mockApplications := NewMockApplicationsServiceInterface(ctrl) mockApplicationStatistics := NewMockApplicationStatisticsServiceInterface(ctrl) + mockAttestations := NewMockAttestationsServiceInterface(ctrl) mockAuditEvents := NewMockAuditEventsServiceInterface(ctrl) mockAvatar := NewMockAvatarRequestsServiceInterface(ctrl) mockAwardEmoji := NewMockAwardEmojiServiceInterface(ctrl) @@ -328,6 +330,7 @@ func newTestClientWithCtrl(ctrl *gomock.Controller, options ...gitlab.ClientOpti Appearance: mockAppearance, Applications: mockApplications, ApplicationStatistics: mockApplicationStatistics, + Attestations: mockAttestations, AuditEvents: mockAuditEvents, Avatar: mockAvatar, AwardEmoji: mockAwardEmoji, @@ -495,6 +498,7 @@ func newTestClientWithCtrl(ctrl *gomock.Controller, options ...gitlab.ClientOpti MockAppearance: mockAppearance, MockApplications: mockApplications, MockApplicationStatistics: mockApplicationStatistics, + MockAttestations: mockAttestations, MockAuditEvents: mockAuditEvents, MockAvatar: mockAvatar, MockAwardEmoji: mockAwardEmoji, -- GitLab From 4a6cc124e96d122a4ee8816602677bb5b9fb99bb Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Fri, 28 Nov 2025 10:34:56 +1300 Subject: [PATCH 02/13] Make expectedOut constant --- attestations_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attestations_test.go b/attestations_test.go index 0cdc40ff..7ec3124f 100644 --- a/attestations_test.go +++ b/attestations_test.go @@ -78,7 +78,7 @@ func TestDownloadAttestation(t *testing.T) { t.Parallel() mux, client := setup(t) - expectedOut := "expected_output" + const expectedOut = "expected_output" mux.HandleFunc("/api/v4/projects/1337/attestations/1/download", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) -- GitLab From ad3ea7c06fed133771d5fe3bf98a4c0be092dd0e Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 09:54:06 +1300 Subject: [PATCH 03/13] Modify code to align to new setup --- attestations.go | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/attestations.go b/attestations.go index cd1aeb83..75ad7b44 100644 --- a/attestations.go +++ b/attestations.go @@ -18,7 +18,6 @@ package gitlab import ( "bytes" - "fmt" "net/http" "time" ) @@ -79,23 +78,10 @@ type DownloadAttestationOptions struct { } func (s *AttestationsService) DownloadAttestation(pid any, opt *DownloadAttestationOptions, options ...RequestOptionFunc) ([]byte, *Response, error) { - project, err := parseID(pid) - if err != nil { - return nil, nil, err - } - - url := fmt.Sprintf("projects/%s/attestations/%d/download", PathEscape(project), opt.AttestationIID) - - req, err := s.client.NewRequest(http.MethodGet, url, nil, options) - if err != nil { - return nil, nil, err - } - - var body bytes.Buffer - response, err := s.client.Do(req, &body) - if err != nil { - return nil, nil, err - } + b, resp, err := do[bytes.Buffer](s.client, + withPath("projects/%s/attestations/%d/download", pid, opt.AttestationIID), + withRequestOpts(options...), + ) - return body.Bytes(), response, err + return b.Bytes(), resp, err } -- GitLab From 245625996924322cb9ca3fb9f665cdbd21fc5169 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 10:05:22 +1300 Subject: [PATCH 04/13] Replace int with int64 --- attestations.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/attestations.go b/attestations.go index 75ad7b44..32ddf738 100644 --- a/attestations.go +++ b/attestations.go @@ -47,10 +47,10 @@ type ( var _ AttestationsServiceInterface = (*AttestationsService)(nil) type Attestation struct { - ID int `json:"id"` - IID int `json:"iid"` - ProjectID int `json:"project_id"` - BuildID int `json:"build_id"` + ID int64 `json:"id"` + IID int64 `json:"iid"` + ProjectID int64 `json:"project_id"` + BuildID int64 `json:"build_id"` Status string `json:"status"` CreatedAt *time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` @@ -74,7 +74,7 @@ func (s *AttestationsService) ListAttestations(pid any, opt *ListAttestationsOpt } type DownloadAttestationOptions struct { - AttestationIID int + AttestationIID int64 } func (s *AttestationsService) DownloadAttestation(pid any, opt *DownloadAttestationOptions, options ...RequestOptionFunc) ([]byte, *Response, error) { -- GitLab From 5216e8b0c83d12ebbeb04caca20aeee8c79dc724 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 10:06:11 +1300 Subject: [PATCH 05/13] Remove copyright header --- attestations.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/attestations.go b/attestations.go index 32ddf738..44e6c9b0 100644 --- a/attestations.go +++ b/attestations.go @@ -1,19 +1,3 @@ -// -// Copyright 2021, Sander van Harmelen -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - package gitlab import ( -- GitLab From 81f66be5cf33306f9b3b40a2df9d9117e4d118e2 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 10:07:20 +1300 Subject: [PATCH 06/13] Remove copyright header from attestation_test.go --- attestations_test.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/attestations_test.go b/attestations_test.go index 7ec3124f..10873046 100644 --- a/attestations_test.go +++ b/attestations_test.go @@ -1,19 +1,3 @@ -// -// Copyright 2021, Sander van Harmelen -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - package gitlab import ( -- GitLab From 0069e6ecb8cb162aa4f6c64fafe6e7afc8e3fd2b Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 10:17:30 +1300 Subject: [PATCH 07/13] Modify code to pass path parameters as arguments --- attestations.go | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/attestations.go b/attestations.go index 44e6c9b0..a0dc7868 100644 --- a/attestations.go +++ b/attestations.go @@ -11,12 +11,12 @@ type ( // ListAttestations gets a list of all attestations // // GitLab API docs: https://docs.gitlab.com/api/attestations - ListAttestations(pid any, opt *ListAttestationsOptions, options ...RequestOptionFunc) ([]*Attestation, *Response, error) + ListAttestations(pid any, subjectDigest string, options ...RequestOptionFunc) ([]*Attestation, *Response, error) // DownloadAttestation // // GitLab API docs: https://docs.gitlab.com/api/attestations - DownloadAttestation(pid any, opt *DownloadAttestationOptions, options ...RequestOptionFunc) ([]byte, *Response, error) + DownloadAttestation(pid any, attestationIID int64, options ...RequestOptionFunc) ([]byte, *Response, error) } // AttestationsService handles communication with the keys related methods @@ -45,25 +45,17 @@ type Attestation struct { DownloadURL string `json:"download_url"` } -type ListAttestationsOptions struct { - SubjectDigest string -} - -func (s *AttestationsService) ListAttestations(pid any, opt *ListAttestationsOptions, options ...RequestOptionFunc) ([]*Attestation, *Response, error) { +func (s *AttestationsService) ListAttestations(pid any, subjectDigest string, options ...RequestOptionFunc) ([]*Attestation, *Response, error) { return do[[]*Attestation](s.client, withMethod(http.MethodGet), - withPath("projects/%s/attestations/%s", pid, opt.SubjectDigest), + withPath("projects/%s/attestations/%s", pid, subjectDigest), withRequestOpts(options...), ) } -type DownloadAttestationOptions struct { - AttestationIID int64 -} - -func (s *AttestationsService) DownloadAttestation(pid any, opt *DownloadAttestationOptions, options ...RequestOptionFunc) ([]byte, *Response, error) { +func (s *AttestationsService) DownloadAttestation(pid any, attestationIID int64, options ...RequestOptionFunc) ([]byte, *Response, error) { b, resp, err := do[bytes.Buffer](s.client, - withPath("projects/%s/attestations/%d/download", pid, opt.AttestationIID), + withPath("projects/%s/attestations/%d/download", pid, attestationIID), withRequestOpts(options...), ) -- GitLab From 0d66e331aab7101e2c49451ad2c501399ef8559b Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 10:19:33 +1300 Subject: [PATCH 08/13] Make tests pass --- attestations_test.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/attestations_test.go b/attestations_test.go index 10873046..d02785d3 100644 --- a/attestations_test.go +++ b/attestations_test.go @@ -32,10 +32,7 @@ func TestListAttestations(t *testing.T) { ]`) }) - listAttestationOptions := ListAttestationsOptions{ - SubjectDigest: "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c", - } - attestations, resp, err := client.Attestations.ListAttestations("1337", &listAttestationOptions) + attestations, resp, err := client.Attestations.ListAttestations("1337", "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c",) assert.NoError(t, err) assert.NotNil(t, resp) @@ -69,10 +66,7 @@ func TestDownloadAttestation(t *testing.T) { fmt.Fprintf(w, expectedOut) }) - downloadAttestationsOptions := DownloadAttestationOptions{ - AttestationIID: 1, - } - outBytes, resp, err := client.Attestations.DownloadAttestation("1337", &downloadAttestationsOptions) + outBytes, resp, err := client.Attestations.DownloadAttestation("1337", 1) assert.NoError(t, err) assert.NotNil(t, resp) -- GitLab From f55c28f3c58f15f0f433cac514ac3140f8290808 Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Mon, 1 Dec 2025 10:26:34 +1300 Subject: [PATCH 09/13] Make reviewable again --- attestations.go | 8 ++++---- attestations_test.go | 2 +- testing/attestations_mock.go | 24 ++++++++++++------------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/attestations.go b/attestations.go index a0dc7868..f133d8ec 100644 --- a/attestations.go +++ b/attestations.go @@ -31,10 +31,10 @@ type ( var _ AttestationsServiceInterface = (*AttestationsService)(nil) type Attestation struct { - ID int64 `json:"id"` - IID int64 `json:"iid"` - ProjectID int64 `json:"project_id"` - BuildID int64 `json:"build_id"` + ID int64 `json:"id"` + IID int64 `json:"iid"` + ProjectID int64 `json:"project_id"` + BuildID int64 `json:"build_id"` Status string `json:"status"` CreatedAt *time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` diff --git a/attestations_test.go b/attestations_test.go index d02785d3..b1a40d9f 100644 --- a/attestations_test.go +++ b/attestations_test.go @@ -32,7 +32,7 @@ func TestListAttestations(t *testing.T) { ]`) }) - attestations, resp, err := client.Attestations.ListAttestations("1337", "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c",) + attestations, resp, err := client.Attestations.ListAttestations("1337", "76c34666f719ef14bd2b124a7db51e9c05e4db2e12a84800296d559064eebe2c") assert.NoError(t, err) assert.NotNil(t, resp) diff --git a/testing/attestations_mock.go b/testing/attestations_mock.go index 0ce49515..6d73cd55 100644 --- a/testing/attestations_mock.go +++ b/testing/attestations_mock.go @@ -40,9 +40,9 @@ func (m *MockAttestationsServiceInterface) EXPECT() *MockAttestationsServiceInte } // DownloadAttestation mocks base method. -func (m *MockAttestationsServiceInterface) DownloadAttestation(pid any, opt *gitlab.DownloadAttestationOptions, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error) { +func (m *MockAttestationsServiceInterface) DownloadAttestation(pid any, attestationIID int64, options ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error) { m.ctrl.T.Helper() - varargs := []any{pid, opt} + varargs := []any{pid, attestationIID} for _, a := range options { varargs = append(varargs, a) } @@ -54,9 +54,9 @@ func (m *MockAttestationsServiceInterface) DownloadAttestation(pid any, opt *git } // DownloadAttestation indicates an expected call of DownloadAttestation. -func (mr *MockAttestationsServiceInterfaceMockRecorder) DownloadAttestation(pid, opt any, options ...any) *MockAttestationsServiceInterfaceDownloadAttestationCall { +func (mr *MockAttestationsServiceInterfaceMockRecorder) DownloadAttestation(pid, attestationIID any, options ...any) *MockAttestationsServiceInterfaceDownloadAttestationCall { mr.mock.ctrl.T.Helper() - varargs := append([]any{pid, opt}, options...) + varargs := append([]any{pid, attestationIID}, options...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadAttestation", reflect.TypeOf((*MockAttestationsServiceInterface)(nil).DownloadAttestation), varargs...) return &MockAttestationsServiceInterfaceDownloadAttestationCall{Call: call} } @@ -73,21 +73,21 @@ func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) Return(arg0 [] } // Do rewrite *gomock.Call.Do -func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) Do(f func(any, *gitlab.DownloadAttestationOptions, ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)) *MockAttestationsServiceInterfaceDownloadAttestationCall { +func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) Do(f func(any, int64, ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)) *MockAttestationsServiceInterfaceDownloadAttestationCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) DoAndReturn(f func(any, *gitlab.DownloadAttestationOptions, ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)) *MockAttestationsServiceInterfaceDownloadAttestationCall { +func (c *MockAttestationsServiceInterfaceDownloadAttestationCall) DoAndReturn(f func(any, int64, ...gitlab.RequestOptionFunc) ([]byte, *gitlab.Response, error)) *MockAttestationsServiceInterfaceDownloadAttestationCall { c.Call = c.Call.DoAndReturn(f) return c } // ListAttestations mocks base method. -func (m *MockAttestationsServiceInterface) ListAttestations(pid any, opt *gitlab.ListAttestationsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error) { +func (m *MockAttestationsServiceInterface) ListAttestations(pid any, subjectDigest string, options ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error) { m.ctrl.T.Helper() - varargs := []any{pid, opt} + varargs := []any{pid, subjectDigest} for _, a := range options { varargs = append(varargs, a) } @@ -99,9 +99,9 @@ func (m *MockAttestationsServiceInterface) ListAttestations(pid any, opt *gitlab } // ListAttestations indicates an expected call of ListAttestations. -func (mr *MockAttestationsServiceInterfaceMockRecorder) ListAttestations(pid, opt any, options ...any) *MockAttestationsServiceInterfaceListAttestationsCall { +func (mr *MockAttestationsServiceInterfaceMockRecorder) ListAttestations(pid, subjectDigest any, options ...any) *MockAttestationsServiceInterfaceListAttestationsCall { mr.mock.ctrl.T.Helper() - varargs := append([]any{pid, opt}, options...) + varargs := append([]any{pid, subjectDigest}, options...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAttestations", reflect.TypeOf((*MockAttestationsServiceInterface)(nil).ListAttestations), varargs...) return &MockAttestationsServiceInterfaceListAttestationsCall{Call: call} } @@ -118,13 +118,13 @@ func (c *MockAttestationsServiceInterfaceListAttestationsCall) Return(arg0 []*gi } // Do rewrite *gomock.Call.Do -func (c *MockAttestationsServiceInterfaceListAttestationsCall) Do(f func(any, *gitlab.ListAttestationsOptions, ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error)) *MockAttestationsServiceInterfaceListAttestationsCall { +func (c *MockAttestationsServiceInterfaceListAttestationsCall) Do(f func(any, string, ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error)) *MockAttestationsServiceInterfaceListAttestationsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockAttestationsServiceInterfaceListAttestationsCall) DoAndReturn(f func(any, *gitlab.ListAttestationsOptions, ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error)) *MockAttestationsServiceInterfaceListAttestationsCall { +func (c *MockAttestationsServiceInterfaceListAttestationsCall) DoAndReturn(f func(any, string, ...gitlab.RequestOptionFunc) ([]*gitlab.Attestation, *gitlab.Response, error)) *MockAttestationsServiceInterfaceListAttestationsCall { c.Call = c.Call.DoAndReturn(f) return c } -- GitLab From 09c702f766ab56ede016068d4c93bae5cb012a99 Mon Sep 17 00:00:00 2001 From: Timo Furrer Date: Mon, 24 Nov 2025 10:43:01 +0100 Subject: [PATCH 10/13] Merge branch 'fix-double-escape' into 'main' Fix double escaping in paths Closes #2177 See merge request gitlab-org/api/client-go!2583 --- request_handler.go | 2 +- request_handler_test.go | 80 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/request_handler.go b/request_handler.go index ba4340a5..b18929ca 100644 --- a/request_handler.go +++ b/request_handler.go @@ -62,7 +62,7 @@ func withPath(path string, args ...any) doOption { if err != nil { return err } - as[i] = PathEscape(project) + as[i] = project case string: as[i] = PathEscape(v) default: diff --git a/request_handler_test.go b/request_handler_test.go index 1180eab5..d028e67f 100644 --- a/request_handler_test.go +++ b/request_handler_test.go @@ -297,3 +297,83 @@ func TestRequestHandlerWithOptions(t *testing.T) { assert.Equal(t, 200, resp.StatusCode) assert.Len(t, users, 1) } + +func TestDoRequestProjectID(t *testing.T) { + t.Parallel() + mux, client := setup(t) + + // GIVEN + mux.HandleFunc("/api/v4/projects/group%2Fproject", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }) + + // WHEN + _, resp, err := do[none]( + client, + withPath("projects/%s", ProjectID{"group/project"}), + ) + + // THEN + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) +} + +func TestDoRequestGroupID(t *testing.T) { + t.Parallel() + mux, client := setup(t) + + // GIVEN + mux.HandleFunc("/api/v4/groups/sub%2Fgroup", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }) + + // WHEN + _, resp, err := do[none]( + client, + withPath("groups/%s", GroupID{"sub/group"}), + ) + + // THEN + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) +} + +func TestDoRequestRunnerID(t *testing.T) { + t.Parallel() + mux, client := setup(t) + + // GIVEN + mux.HandleFunc("/api/v4/runners/some%2Frunner", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }) + + // WHEN + _, resp, err := do[none]( + client, + withPath("runners/%s", RunnerID{"some/runner"}), + ) + + // THEN + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) +} + +func TestDoRequestUserID(t *testing.T) { + t.Parallel() + mux, client := setup(t) + + // GIVEN + mux.HandleFunc("/api/v4/users/test%2Fuser", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }) + + // WHEN + _, resp, err := do[none]( + client, + withPath("users/%s", UserID{"test/user"}), + ) + + // THEN + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) +} -- GitLab From 4d34eefc371cdd47f2d3410c8f978b947fe9bf2d Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 2 Dec 2025 13:04:16 +1300 Subject: [PATCH 11/13] Use `ProjectID{pid}` again --- attestations.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/attestations.go b/attestations.go index f133d8ec..3b032089 100644 --- a/attestations.go +++ b/attestations.go @@ -48,14 +48,14 @@ type Attestation struct { func (s *AttestationsService) ListAttestations(pid any, subjectDigest string, options ...RequestOptionFunc) ([]*Attestation, *Response, error) { return do[[]*Attestation](s.client, withMethod(http.MethodGet), - withPath("projects/%s/attestations/%s", pid, subjectDigest), + withPath("projects/%s/attestations/%s", ProjectID{pid}, subjectDigest), withRequestOpts(options...), ) } func (s *AttestationsService) DownloadAttestation(pid any, attestationIID int64, options ...RequestOptionFunc) ([]byte, *Response, error) { b, resp, err := do[bytes.Buffer](s.client, - withPath("projects/%s/attestations/%d/download", pid, attestationIID), + withPath("projects/%s/attestations/%d/download", ProjectID{pid}, attestationIID), withRequestOpts(options...), ) -- GitLab From 0e9cf71f9c67f22a23442c66b28d289ed569592d Mon Sep 17 00:00:00 2001 From: Sam Joan Roque-Worcel Date: Tue, 2 Dec 2025 13:06:43 +1300 Subject: [PATCH 12/13] Revert "Merge branch 'fix-double-escape' into 'main'" This reverts commit 09c702f766ab56ede016068d4c93bae5cb012a99. --- request_handler.go | 2 +- request_handler_test.go | 80 ----------------------------------------- 2 files changed, 1 insertion(+), 81 deletions(-) diff --git a/request_handler.go b/request_handler.go index b18929ca..ba4340a5 100644 --- a/request_handler.go +++ b/request_handler.go @@ -62,7 +62,7 @@ func withPath(path string, args ...any) doOption { if err != nil { return err } - as[i] = project + as[i] = PathEscape(project) case string: as[i] = PathEscape(v) default: diff --git a/request_handler_test.go b/request_handler_test.go index d028e67f..1180eab5 100644 --- a/request_handler_test.go +++ b/request_handler_test.go @@ -297,83 +297,3 @@ func TestRequestHandlerWithOptions(t *testing.T) { assert.Equal(t, 200, resp.StatusCode) assert.Len(t, users, 1) } - -func TestDoRequestProjectID(t *testing.T) { - t.Parallel() - mux, client := setup(t) - - // GIVEN - mux.HandleFunc("/api/v4/projects/group%2Fproject", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - - // WHEN - _, resp, err := do[none]( - client, - withPath("projects/%s", ProjectID{"group/project"}), - ) - - // THEN - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) -} - -func TestDoRequestGroupID(t *testing.T) { - t.Parallel() - mux, client := setup(t) - - // GIVEN - mux.HandleFunc("/api/v4/groups/sub%2Fgroup", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - - // WHEN - _, resp, err := do[none]( - client, - withPath("groups/%s", GroupID{"sub/group"}), - ) - - // THEN - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) -} - -func TestDoRequestRunnerID(t *testing.T) { - t.Parallel() - mux, client := setup(t) - - // GIVEN - mux.HandleFunc("/api/v4/runners/some%2Frunner", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - - // WHEN - _, resp, err := do[none]( - client, - withPath("runners/%s", RunnerID{"some/runner"}), - ) - - // THEN - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) -} - -func TestDoRequestUserID(t *testing.T) { - t.Parallel() - mux, client := setup(t) - - // GIVEN - mux.HandleFunc("/api/v4/users/test%2Fuser", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - - // WHEN - _, resp, err := do[none]( - client, - withPath("users/%s", UserID{"test/user"}), - ) - - // THEN - assert.NoError(t, err) - assert.Equal(t, 200, resp.StatusCode) -} -- GitLab From 4a01a8f100c80d26942acf91acfc84f1133c7df3 Mon Sep 17 00:00:00 2001 From: Timo Furrer Date: Tue, 2 Dec 2025 09:13:42 +0100 Subject: [PATCH 13/13] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Patrick Rice --- attestations.go | 1 + 1 file changed, 1 insertion(+) diff --git a/attestations.go b/attestations.go index 3b032089..161a0b2e 100644 --- a/attestations.go +++ b/attestations.go @@ -55,6 +55,7 @@ func (s *AttestationsService) ListAttestations(pid any, subjectDigest string, op func (s *AttestationsService) DownloadAttestation(pid any, attestationIID int64, options ...RequestOptionFunc) ([]byte, *Response, error) { b, resp, err := do[bytes.Buffer](s.client, + withMethod(http.MethodGet), withPath("projects/%s/attestations/%d/download", ProjectID{pid}, attestationIID), withRequestOpts(options...), ) -- GitLab