Skip to content
Commits on Source (336)
......@@ -10,7 +10,7 @@
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/go:1": {
"version": "1.22.10"
"version": "1.23"
}
},
// Extensions for VSCode
......
......@@ -31,3 +31,8 @@ _testmain.go
# vendor
vendor
.go/
.golangci-lint/
# reports
gl-code-quality-report.json
\ No newline at end of file
......@@ -6,11 +6,25 @@ workflow:
- if: $CI_COMMIT_REF_PROTECTED == "true"
include:
- template: Jobs/SAST.gitlab-ci.yml
- component: ${CI_SERVER_FQDN}/gitlab-org/components/danger-review/danger-review@2.0.0
inputs:
job_stage: lint
job_allow_failure: true
# NOTE: the two includes below are a hack to conditionally set the tags node
# on our Go jobs. We want to use the large Ultimate runners if possible,
# which is what we have available in the gitlab-org and gitlab-community (Community Forks)
# groups. However, there is no easy way to conditionally set tags or even variables without
# jeopardizing existing (complex) workflow:rules or job:rules. Thus, we resort to
# this nasty conditionally include hack.
- local: '.gitlab/ci/gitlab-go-runner-tags.gitlab-ci.yml'
rules:
- if: $CI_PROJECT_ROOT_NAMESPACE == 'gitlab-org' || $CI_PROJECT_ROOT_NAMESPACE == 'gitlab-community'
- local: '.gitlab/ci/community-go-runner-tags.gitlab-ci.yml'
rules:
- if: $CI_PROJECT_ROOT_NAMESPACE != 'gitlab-org' && $CI_PROJECT_ROOT_NAMESPACE != 'gitlab-community'
stages:
- lint
- test
......@@ -20,16 +34,19 @@ stages:
parallel:
matrix:
- GOLANG_IMAGE_VERSION:
- '1.22.10'
- '1.23.4'
- '1.23'
- '1.24'
.go:base:
# From: https://docs.gitlab.com/ee/ci/caching/#cache-go-dependencies
extends:
- .go:runner-tags
# From: https://docs.gitlab.com/ci/caching/#cache-go-dependencies
variables:
GOPATH: $CI_PROJECT_DIR/.go
GOLANGCI_LINT_CACHE: $CI_PROJECT_DIR/.golangci-lint
before_script:
- mkdir -p "${GOPATH}" "${GOLANGCI_LINT_CACHE}"
- export PATH="${GOPATH}/bin:$PATH"
cache:
paths:
- $GOPATH/pkg/mod/
......@@ -37,14 +54,6 @@ stages:
key:
files:
- go.sum
# We want to speed up CI a bit.
# Community contributors are recommended to use the Community fork
# which has access to this runners.
# For other forks to free tier namespaces this might fail,
# which is a good reminder to use the Community fork and not
# to accidentally burn to personal compute minutes.
tags:
- saas-linux-large-amd64
# We only need to run Go-related jobs when actual Go files changed
# or when running either on the default branch or for a tag.
rules:
......@@ -56,6 +65,7 @@ stages:
- go.mod
- go.sum
- .gitlab-ci.yml
- .gitlab/ci/*.yml
golangci-lint:
extends:
......@@ -64,15 +74,33 @@ golangci-lint:
needs: []
variables:
REPORT_FILENAME: 'gl-code-quality-report.json'
image: golangci/golangci-lint:v1.63.4
image: golangci/golangci-lint:v2.1.6
script:
- golangci-lint run --print-issued-lines=false --out-format code-climate:$REPORT_FILENAME,line-number
- golangci-lint run
artifacts:
reports:
codequality: $REPORT_FILENAME
paths: [$REPORT_FILENAME]
when: always
verify-generated-code:
extends:
- .go:base
stage: lint
needs: []
image: golang:1.24-bookworm
script:
- make generate
- |
echo "Checking git status"
[ -z "$(git status --short)" ] || {
echo "Error: Files should have been generated:";
git status --short; echo "Diff:";
git --no-pager diff HEAD;
echo "Run \"make generate\" and try again";
exit 1;
}
tests:unit:
extends:
- .go:base
......@@ -91,6 +119,8 @@ tests:unit:
COVERPROFILE_XML_FILENAME: coverage.xml
script:
- go run gotest.tools/gotestsum@${GOTESTSUM_VERSION} --format=standard-quiet --junitfile=$JUNIT_FILENAME -- -race -coverprofile=$COVERPROFILE_FILENAME -covermode=atomic ./...
- grep -v '_generated.go' "$COVERPROFILE_FILENAME" | grep -v '_mock.go' > "${COVERPROFILE_FILENAME}.tmp"
- mv "${COVERPROFILE_FILENAME}.tmp" "$COVERPROFILE_FILENAME"
- go run github.com/boumenot/gocover-cobertura@${GOCOVER_COBERTURA_VERSION} < $COVERPROFILE_FILENAME > $COVERPROFILE_XML_FILENAME
- go tool cover -func $COVERPROFILE_FILENAME
coverage: '/total:.+\(statements\).+\d+\.\d+/'
......@@ -108,20 +138,41 @@ tests:unit:
generate-release-notes:
stage: deploy
needs: []
image: alpine:3.21.2
image: alpine:3.21.3
before_script:
- apk add --update jq curl git
variables:
GIT_DEPTH: 400
GIT_FETCH_EXTRA_FLAGS: '--tags'
script:
- |
# Download upstream tags if running from a fork
if [ "${CI_MERGE_REQUEST_SOURCE_PROJECT_ID}" != "${CI_MERGE_REQUEST_PROJECT_ID}" ]; then
echo "This merge request has been created from a fork."
if [ "${CI_MERGE_REQUEST_SOURCE_PROJECT_ID}" = "${CI_PROJECT_ID}" ]; then
echo "The merge request pipeline runs in the source project. Downloading tags."
git fetch --depth="${GIT_DEPTH}" --tags "${CI_MERGE_REQUEST_PROJECT_URL}"
else
echo "The merge request pipeline runs in the target project. Not downloading tags."
fi
fi
- |
# Determine version.
if [ -z "$CI_COMMIT_TAG" ]; then
last_stable_version_sha="$(git tag | grep -E '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$' | sort -Vr | head -n1)"
version="${last_stable_version_sha}+${CI_COMMIT_SHA}"
version="$(git describe --tags --match 'v*')"
else
version="$CI_COMMIT_TAG"
fi
urlencoded_version="$(jq -rn --arg x "${version}" '$x|@uri')"
- echo "Generating release notes for ${version} (urlencoded=${urlencoded_version}) ..."
- 'curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/repository/changelog?version=${urlencoded_version}" | jq -r .notes > release-notes.md'
echo "Generating release notes for ${version} (urlencoded=${urlencoded_version}) ..."
- |
# If running in a merge request pipeline, generate the release notes using the target project.
PROJECT_ID="${CI_PROJECT_ID}"
if [ -n "${CI_MERGE_REQUEST_PROJECT_ID}" ]; then
PROJECT_ID="${CI_MERGE_REQUEST_PROJECT_ID}"
fi
- url="https://gitlab.com/api/v4/projects/${PROJECT_ID}/repository/changelog?version=${urlencoded_version}"; echo "url=\"${url}\""
- curl --fail-with-body "${url}" | jq -r .notes >release-notes.md
- cat release-notes.md
artifacts:
paths:
......@@ -144,3 +195,11 @@ release:
tag_message: 'Version $CI_COMMIT_TAG'
name: '$CI_COMMIT_TAG'
description: release-notes.md
# Update rules on SAST to ensure the jobs show up in the pipeline
# this prevents forks that don't have `ultimate` from skipping SAST scans
# since gitlab-advaced-sast replaces semgrep.
semgrep-sast:
needs: []
rules:
- when: always
* @timofurrer @patrickrice @fforster
# See https://docs.gitlab.com/ee/user/project/changelogs.html
# For API see https://docs.gitlab.com/ee/api/repositories.html#add-changelog-data-to-a-changelog-file
# See https://docs.gitlab.com/user/project/changelogs/
# For API see https://docs.gitlab.com/api/repositories/#add-changelog-data-to-a-changelog-file
tag_regex: '^v(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<pre>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<meta>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
.go:runner-tags:
tags:
- saas-linux-medium-amd64
.go:runner-tags:
tags:
- saas-linux-large-amd64
# This file contains all available configuration options
# with their default values.
version: "2"
# Options for analysis running
run:
concurrency: 4
timeout: 10m
issues-exit-code: 1
tests: true
# Output configuration options
output:
formats:
- format: line-number
# All available settings of specific linters
linters-settings:
misspell:
locale: US
ignore-words:
- noteable
revive:
enable-all-rules: false
rules:
- name: deep-exit
text:
path: stdout
colors: false
print-issued-lines: false
code-climate:
path: gl-code-quality-report.json
linters:
enable:
- asciicheck
- dogsled
- errorlint
- exportloopref
- goconst
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- revive
- staticcheck
- typecheck
- unconvert
- unused
- whitespace
disable:
- errcheck
disable-all: false
fast: false
settings:
misspell:
locale: US
ignore-rules:
- noteable
revive:
enable-all-rules: false
rules:
- name: deep-exit
issues:
# List of regexps of issue texts to exclude.
exclude:
- "^.*, make it a constant$"
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- path: (.+)\.go$
text: ^.*, make it a constant$
paths:
- third_party$
- builtin$
- examples/*
issues:
# Maximum issues count per one linter (set to 0 to disable)
max-issues-per-linter: 0
# Maximum count of issues with the same text (set to 0 to disable)
max-same-issues: 0
formatters:
enable:
- gofumpt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
golang 1.22.10
golang 1.23
......@@ -45,19 +45,14 @@ easier to find things.
### Setting up your local development environment to contribute
1. [Fork](https://gitlab.com/gitlab-org/api/client-go), then clone the repository.
```sh
git clone https://gitlab.com/<your-username>/client-go.git
# or via ssh
git clone git@gitlab.com:<your-username>/client-go.git
```
1. Install dependencies:
```sh
make setup
```
1. Make your changes on your feature branch
1. Run the tests and `gofumpt`
1. Make your changes on your feature branch in the community fork or your personal fork
1. Run the reviewable command, which tests, lints and formats the code:
```sh
make test && make fmt
make reviewable
```
1. Push your feature branch upstream
1. Open up your merge request
require 'gitlab-dangerfiles'
# see https://docs.gitlab.com/ee/development/dangerbot.html#enable-danger-on-a-project
# see https://docs.gitlab.com/development/dangerbot/#enable-danger-on-a-project
# see https://gitlab.com/gitlab-org/ruby/gems/gitlab-dangerfiles
Gitlab::Dangerfiles.for_project(self) do |dangerfiles|
Gitlab::Dangerfiles.for_project(self, 'gitlab-api-client-go') do |dangerfiles|
# Import all plugins from the gem
dangerfiles.import_plugins
# Import a defined set of danger rules
dangerfiles.import_dangerfiles(only: %w[changelog metadata type_label z_add_labels z_retry_link])
dangerfiles.import_dangerfiles(only: %w[simple_roulette changelog metadata type_label z_add_labels z_retry_link])
end
......@@ -6,17 +6,36 @@ help: ## Display this help
##@ Development
fmt: ## Format code
@gofumpt -l -w .
reviewable: setup generate fmt lint test ## Run before committing.
lint: ## Run linter
fmt: install-gofumpt ## Format code
@gofumpt -l -w *.go testing/*.go examples/*.go
lint: install-golangci-lint ## Run linter
@golangci-lint run
setup: ## Setup your local environment
.PHONY: setup
setup: install-golangci-lint install-gofumpt ## Setup your local environment
go mod tidy
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
install-golangci-lint:
@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
install-gofumpt:
@go install mvdan.cc/gofumpt@latest
.PHONY: setup
.PHONY: generate
generate: install-gofumpt ## Generate files
./scripts/generate_testing_client.sh
./scripts/generate_service_interface_map.sh
./scripts/generate_mock_api.sh
.PHONY: clean
clean: ## Remove generated files
rm -f \
testing/*_mock.go \
testing/*_generated.go \
*_generated_test.go
test: ## Run tests
go test ./... -race
......@@ -2,6 +2,11 @@
A GitLab API client enabling Go programs to interact with GitLab in a simple and uniform way.
## Table of Contents
[[_TOC_]]
## Usage
```go
......@@ -89,6 +94,69 @@ func main() {
For complete usage of go-gitlab, see the full [package docs](https://godoc.org/gitlab.com/gitlab-org/api/client-go).
## Installation
To install the library, use the following command:
```go
go get gitlab.com/gitlab-org/api/client-go
```
## Testing
The `client-go` project comes with a `testing` package at `gitlab.com/gitlab-org/api/client-go/testing`
which contains a `TestClient` with [gomock](https://github.com/uber-go/mock) mocks for the individual services.
You can use them like this:
```go
func Test_MyApp(t *testing.T) {
client := testing.NewTestClient(t)
// Setup expectations
client.MockClusterAgents.EXPECT().
List(gomock.Any(), 123, nil).
Return([]*gitlab.ClusterAgent{{ID: 1}}, nil)
// Use the client in your test
// You'd probably call your own code here that gets the client injected.
// You can also retrieve a `gitlab.Client` object from `client.Client`.
agents, err := client.ClusterAgents.List(ctx, 123, nil)
assert.NoError(t, err)
assert.Len(t, agents, 1)
}
```
### I want to generate my own mocks
You can! You can set up your own `TestClient` with mocks pretty easily:
```go
func NewTestClient(t *testing.T) {
// generate your mocks or instantiate a fake or whatever you like
mockClusterAgentsService := newMockClusterAgentsService(t)
client := &gitlab.Client{
ClusterAgents: mockClusterAgentsService
}
return tc
}
```
The `newMockClusterAgentsService` must return a type that implements `gitlab.ClusterAgentsInterface`.
You can have a look at [`testing/client.go`](/testing.client.go) how it's implemented for `gomock`.
## Compatibility
The `client-go` package will maintain compatibility with the officially supported Go releases
at the time the package is released. According to the [Go Release Policy](https://go.dev/doc/devel/release#policy),
that's currently the two last major Go releases.
This compatibility is reflected in the `go` directive of the [`go.mod`](/go.mod) file
and the unit test matrix in [`.gitlab-ci.yml`](/.gitlab-ci.yml).
You may also use https://endoflife.date/go to quickly discover the supported Go versions.
## Contributing
Contributions are always welcome. For more information, check out the
......
......@@ -22,10 +22,33 @@ import (
"time"
)
type (
AccessRequestsServiceInterface interface {
ListProjectAccessRequests(pid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error)
ListGroupAccessRequests(gid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error)
RequestProjectAccess(pid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
RequestGroupAccess(gid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
ApproveProjectAccessRequest(pid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
ApproveGroupAccessRequest(gid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error)
DenyProjectAccessRequest(pid any, user int, options ...RequestOptionFunc) (*Response, error)
DenyGroupAccessRequest(gid any, user int, options ...RequestOptionFunc) (*Response, error)
}
// AccessRequestsService handles communication with the project/group
// access requests related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/api/access_requests/
AccessRequestsService struct {
client *Client
}
)
var _ AccessRequestsServiceInterface = (*AccessRequestsService)(nil)
// AccessRequest represents a access request for a group or project.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html
// https://docs.gitlab.com/api/access_requests/
type AccessRequest struct {
ID int `json:"id"`
Username string `json:"username"`
......@@ -36,27 +59,19 @@ type AccessRequest struct {
AccessLevel AccessLevelValue `json:"access_level"`
}
// AccessRequestsService handles communication with the project/group
// access requests related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/ee/api/access_requests.html
type AccessRequestsService struct {
client *Client
}
// ListAccessRequestsOptions represents the available
// ListProjectAccessRequests() or ListGroupAccessRequests() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#list-access-requests-for-a-group-or-project
// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
type ListAccessRequestsOptions ListOptions
// ListProjectAccessRequests gets a list of access requests
// viewable by the authenticated user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#list-access-requests-for-a-group-or-project
func (s *AccessRequestsService) ListProjectAccessRequests(pid interface{}, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error) {
// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
func (s *AccessRequestsService) ListProjectAccessRequests(pid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
......@@ -81,8 +96,8 @@ func (s *AccessRequestsService) ListProjectAccessRequests(pid interface{}, opt *
// viewable by the authenticated user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#list-access-requests-for-a-group-or-project
func (s *AccessRequestsService) ListGroupAccessRequests(gid interface{}, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error) {
// https://docs.gitlab.com/api/access_requests/#list-access-requests-for-a-group-or-project
func (s *AccessRequestsService) ListGroupAccessRequests(gid any, opt *ListAccessRequestsOptions, options ...RequestOptionFunc) ([]*AccessRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
......@@ -107,8 +122,8 @@ func (s *AccessRequestsService) ListGroupAccessRequests(gid interface{}, opt *Li
// to a group or project.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#request-access-to-a-group-or-project
func (s *AccessRequestsService) RequestProjectAccess(pid interface{}, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
// https://docs.gitlab.com/api/access_requests/#request-access-to-a-group-or-project
func (s *AccessRequestsService) RequestProjectAccess(pid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
......@@ -133,8 +148,8 @@ func (s *AccessRequestsService) RequestProjectAccess(pid interface{}, options ..
// to a group or project.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#request-access-to-a-group-or-project
func (s *AccessRequestsService) RequestGroupAccess(gid interface{}, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
// https://docs.gitlab.com/api/access_requests/#request-access-to-a-group-or-project
func (s *AccessRequestsService) RequestGroupAccess(gid any, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
......@@ -159,7 +174,7 @@ func (s *AccessRequestsService) RequestGroupAccess(gid interface{}, options ...R
// ApproveProjectAccessRequest() and ApproveGroupAccessRequest() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#approve-an-access-request
// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
type ApproveAccessRequestOptions struct {
AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
}
......@@ -167,8 +182,8 @@ type ApproveAccessRequestOptions struct {
// ApproveProjectAccessRequest approves an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#approve-an-access-request
func (s *AccessRequestsService) ApproveProjectAccessRequest(pid interface{}, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
func (s *AccessRequestsService) ApproveProjectAccessRequest(pid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
......@@ -192,8 +207,8 @@ func (s *AccessRequestsService) ApproveProjectAccessRequest(pid interface{}, use
// ApproveGroupAccessRequest approves an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#approve-an-access-request
func (s *AccessRequestsService) ApproveGroupAccessRequest(gid interface{}, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
// https://docs.gitlab.com/api/access_requests/#approve-an-access-request
func (s *AccessRequestsService) ApproveGroupAccessRequest(gid any, user int, opt *ApproveAccessRequestOptions, options ...RequestOptionFunc) (*AccessRequest, *Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, nil, err
......@@ -217,8 +232,8 @@ func (s *AccessRequestsService) ApproveGroupAccessRequest(gid interface{}, user
// DenyProjectAccessRequest denies an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#deny-an-access-request
func (s *AccessRequestsService) DenyProjectAccessRequest(pid interface{}, user int, options ...RequestOptionFunc) (*Response, error) {
// https://docs.gitlab.com/api/access_requests/#deny-an-access-request
func (s *AccessRequestsService) DenyProjectAccessRequest(pid any, user int, options ...RequestOptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
......@@ -236,8 +251,8 @@ func (s *AccessRequestsService) DenyProjectAccessRequest(pid interface{}, user i
// DenyGroupAccessRequest denies an access request for the given user.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/access_requests.html#deny-an-access-request
func (s *AccessRequestsService) DenyGroupAccessRequest(gid interface{}, user int, options ...RequestOptionFunc) (*Response, error) {
// https://docs.gitlab.com/api/access_requests/#deny-an-access-request
func (s *AccessRequestsService) DenyGroupAccessRequest(gid any, user int, options ...RequestOptionFunc) (*Response, error) {
group, err := parseID(gid)
if err != nil {
return nil, err
......
......@@ -27,6 +27,7 @@ import (
)
func TestListProjectAccessRequests(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/access_requests", func(w http.ResponseWriter, r *http.Request) {
......@@ -71,28 +72,58 @@ func TestListProjectAccessRequests(t *testing.T) {
},
}
requests, resp, err := client.AccessRequests.ListProjectAccessRequests(1, nil)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, expected, requests)
tests := []struct {
name string
projectID any
expectedErr string
expectedRes []*AccessRequest
statusCode int
}{
{
name: "Valid Project ID",
projectID: 1,
expectedErr: "",
expectedRes: expected,
statusCode: http.StatusOK,
},
{
name: "Invalid Project ID Type",
projectID: 1.5,
expectedErr: "invalid ID type 1.5, the ID must be an int or a string",
expectedRes: nil,
statusCode: 0,
},
{
name: "Non-Existent Project ID",
projectID: 2,
expectedErr: "404 Not Found",
expectedRes: nil,
statusCode: http.StatusNotFound,
},
}
requests, resp, err = client.AccessRequests.ListProjectAccessRequests(1.5, nil)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.Nil(t, resp)
assert.Nil(t, requests)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requests, resp, err := client.AccessRequests.ListProjectAccessRequests(tt.projectID, nil)
requests, resp, err = client.AccessRequests.ListProjectAccessRequests(2, nil)
assert.Error(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
if tt.expectedErr != "" {
assert.EqualError(t, err, tt.expectedErr)
assert.Nil(t, requests)
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, tt.expectedRes, requests)
}
requests, resp, err = client.AccessRequests.ListProjectAccessRequests(1, nil, errorOption)
assert.EqualError(t, err, "RequestOptionFunc returns an error")
assert.Nil(t, resp)
assert.Nil(t, requests)
if tt.statusCode != 0 && resp != nil {
assert.Equal(t, tt.statusCode, resp.StatusCode)
}
})
}
}
func TestListGroupAccessRequests(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/groups/1/access_requests", func(w http.ResponseWriter, r *http.Request) {
......@@ -143,7 +174,7 @@ func TestListGroupAccessRequests(t *testing.T) {
assert.Equal(t, expected, requests)
requests, resp, err = client.AccessRequests.ListGroupAccessRequests(1.5, nil)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
assert.Nil(t, requests)
......@@ -159,6 +190,7 @@ func TestListGroupAccessRequests(t *testing.T) {
}
func TestRequestProjectAccess(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/access_requests", func(w http.ResponseWriter, r *http.Request) {
......@@ -189,7 +221,7 @@ func TestRequestProjectAccess(t *testing.T) {
assert.Equal(t, expected, accessRequest)
accessRequest, resp, err = client.AccessRequests.RequestProjectAccess(1.5, nil)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
assert.Nil(t, accessRequest)
......@@ -205,6 +237,7 @@ func TestRequestProjectAccess(t *testing.T) {
}
func TestRequestGroupAccess(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/groups/1/access_requests", func(w http.ResponseWriter, r *http.Request) {
......@@ -235,7 +268,7 @@ func TestRequestGroupAccess(t *testing.T) {
assert.Equal(t, expected, accessRequest)
accessRequest, resp, err = client.AccessRequests.RequestGroupAccess(1.5, nil)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
assert.Nil(t, accessRequest)
......@@ -251,6 +284,7 @@ func TestRequestGroupAccess(t *testing.T) {
}
func TestApproveProjectAccessRequest(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/access_requests/10/approve", func(w http.ResponseWriter, r *http.Request) {
......@@ -294,7 +328,7 @@ func TestApproveProjectAccessRequest(t *testing.T) {
assert.Equal(t, expected, request)
request, resp, err = client.AccessRequests.ApproveProjectAccessRequest(1.5, 10, opt)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
assert.Nil(t, request)
......@@ -310,6 +344,7 @@ func TestApproveProjectAccessRequest(t *testing.T) {
}
func TestApproveGroupAccessRequest(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/groups/1/access_requests/10/approve", func(w http.ResponseWriter, r *http.Request) {
......@@ -353,7 +388,7 @@ func TestApproveGroupAccessRequest(t *testing.T) {
assert.Equal(t, expected, request)
request, resp, err = client.AccessRequests.ApproveGroupAccessRequest(1.5, 10, opt)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
assert.Nil(t, request)
......@@ -369,6 +404,7 @@ func TestApproveGroupAccessRequest(t *testing.T) {
}
func TestDenyProjectAccessRequest(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/access_requests/10", func(w http.ResponseWriter, r *http.Request) {
......@@ -380,7 +416,7 @@ func TestDenyProjectAccessRequest(t *testing.T) {
assert.NotNil(t, resp)
resp, err = client.AccessRequests.DenyProjectAccessRequest(1.5, 10)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
resp, err = client.AccessRequests.DenyProjectAccessRequest(2, 10)
......@@ -393,6 +429,7 @@ func TestDenyProjectAccessRequest(t *testing.T) {
}
func TestDenyGroupAccessRequest(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/groups/1/access_requests/10", func(w http.ResponseWriter, r *http.Request) {
......@@ -404,7 +441,7 @@ func TestDenyGroupAccessRequest(t *testing.T) {
assert.NotNil(t, resp)
resp, err = client.AccessRequests.DenyGroupAccessRequest(1.5, 10)
assert.EqualError(t, err, "invalid ID type 1.5, the ID must be an int or a string")
assert.ErrorIs(t, err, ErrInvalidIDType)
assert.Nil(t, resp)
resp, err = client.AccessRequests.DenyGroupAccessRequest(2, 10)
......
//
// 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"
"io"
"net/http"
"time"
)
type (
AlertManagementServiceInterface interface {
UploadMetricImage(pid any, alertIID int, content io.Reader, filename string, opt *UploadMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error)
ListMetricImages(pid any, alertIID int, opt *ListMetricImagesOptions, options ...RequestOptionFunc) ([]*MetricImage, *Response, error)
UpdateMetricImage(pid any, alertIID int, id int, opt *UpdateMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error)
DeleteMetricImage(pid any, alertIID int, id int, options ...RequestOptionFunc) (*Response, error)
}
// AlertManagementService handles communication with the alert management
// related methods of the GitLab API.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/
AlertManagementService struct {
client *Client
}
)
var _ AlertManagementServiceInterface = (*AlertManagementService)(nil)
// MetricImage represents a single metric image file.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/
type MetricImage struct {
ID int `json:"id"`
CreatedAt *time.Time `json:"created_at"`
Filename string `json:"filename"`
FilePath string `json:"file_path"`
URL string `json:"url"`
URLText string `json:"url_text"`
}
// UploadMetricImageOptions represents the available UploadMetricImage() options.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#upload-metric-image
type UploadMetricImageOptions struct {
URL *string `url:"url,omitempty" json:"url,omitempty"`
URLText *string `url:"url_text,omitempty" json:"url_text,omitempty"`
}
// UploadMetricImageOptions uploads a metric image to a project alert.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#upload-metric-image
func (s *AlertManagementService) UploadMetricImage(pid any, alertIID int, content io.Reader, filename string, opt *UploadMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/alert_management_alerts/%d/metric_images", PathEscape(project), alertIID)
req, err := s.client.UploadRequest(http.MethodPost, u, content, filename, UploadFile, opt, options)
if err != nil {
return nil, nil, err
}
mi := new(MetricImage)
resp, err := s.client.Do(req, mi)
if err != nil {
return nil, resp, err
}
return mi, resp, nil
}
// ListMetricImagesOptions represents the available ListMetricImages() options.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#list-metric-images
type ListMetricImagesOptions struct {
ListOptions
}
// ListMetricImages lists all the metric images for a project alert.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#list-metric-images
func (s *AlertManagementService) ListMetricImages(pid any, alertIID int, opt *ListMetricImagesOptions, options ...RequestOptionFunc) ([]*MetricImage, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/alert_management_alerts/%d/metric_images", PathEscape(project), alertIID)
req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
if err != nil {
return nil, nil, err
}
var mis []*MetricImage
resp, err := s.client.Do(req, &mis)
if err != nil {
return nil, resp, err
}
return mis, resp, nil
}
// UpdateMetricImageOptions represents the available UpdateMetricImage() options.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#update-metric-image
type UpdateMetricImageOptions struct {
URL *string `url:"url,omitempty" json:"url,omitempty"`
URLText *string `url:"url_text,omitempty" json:"url_text,omitempty"`
}
// UpdateMetricImage updates a metric image for a project alert.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#update-metric-image
func (s *AlertManagementService) UpdateMetricImage(pid any, alertIID int, id int, opt *UpdateMetricImageOptions, options ...RequestOptionFunc) (*MetricImage, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/alert_management_alerts/%d/metric_images/%d", PathEscape(project), alertIID, id)
req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
if err != nil {
return nil, nil, err
}
mi := new(MetricImage)
resp, err := s.client.Do(req, mi)
if err != nil {
return nil, resp, err
}
return mi, resp, nil
}
// DeleteMetricImage deletes a metric image for a project alert.
//
// GitLab API docs:
// https://docs.gitlab.com/api/alert_management_alerts/#delete-metric-image
func (s *AlertManagementService) DeleteMetricImage(pid any, alertIID int, id int, options ...RequestOptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/alert_management_alerts/%d/metric_images/%d", PathEscape(project), alertIID, id)
req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
package gitlab
import (
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAlertManagement_UploadMetricImage(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/alert_management_alerts/2/metric_images", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
fmt.Fprint(w, `
{
"id":17,
"created_at":"2020-11-12T20:07:58.000Z",
"filename":"sample_2054",
"file_path":"/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
"url":"https://example.com/metric",
"url_text":"An example metric"
}
`)
})
createdAt := time.Date(2020, 11, 12, 20, 7, 58, 0, time.UTC)
want := &MetricImage{
ID: 17,
CreatedAt: &createdAt,
Filename: "sample_2054",
FilePath: "/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
URL: "https://example.com/metric",
URLText: "An example metric",
}
metricImage, resp, err := client.AlertManagement.UploadMetricImage(1, 2, strings.NewReader("image"), "sample_2054", &UploadMetricImageOptions{
URL: Ptr("https://example.com/metric"),
URLText: Ptr("An example metric"),
})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, want, metricImage)
}
func TestAlertManagement_ListMetricImages(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/alert_management_alerts/2/metric_images", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
fmt.Fprint(w, `
[
{
"id":17,
"created_at":"2020-11-12T20:07:58.000Z",
"filename":"sample_2054",
"file_path":"/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
"url":"https://example.com/metric",
"url_text":"An example metric"
},
{
"id":18,
"created_at":"2020-11-12T20:07:58.000Z",
"filename":"sample_2054",
"file_path":"/uploads/-/system/alert_metric_image/file/18/sample_2054.png",
"url":"https://example.com/metric",
"url_text":"An example metric"
}
]
`)
})
createdAt := time.Date(2020, 11, 12, 20, 7, 58, 0, time.UTC)
want := []*MetricImage{
{
ID: 17,
CreatedAt: &createdAt,
Filename: "sample_2054",
FilePath: "/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
URL: "https://example.com/metric",
URLText: "An example metric",
},
{
ID: 18,
CreatedAt: &createdAt,
Filename: "sample_2054",
FilePath: "/uploads/-/system/alert_metric_image/file/18/sample_2054.png",
URL: "https://example.com/metric",
URLText: "An example metric",
},
}
metricImages, resp, err := client.AlertManagement.ListMetricImages(1, 2, nil)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, want, metricImages)
}
func TestAlertManagement_UpdateMetricImage(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/alert_management_alerts/2/metric_images/17", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPut)
fmt.Fprint(w, `
{
"id":17,
"created_at":"2020-11-12T20:07:58.000Z",
"filename":"sample_2054",
"file_path":"/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
"url":"https://example.com/metric",
"url_text":"An example metric"
}
`)
})
createdAt := time.Date(2020, 11, 12, 20, 7, 58, 0, time.UTC)
want := &MetricImage{
ID: 17,
CreatedAt: &createdAt,
Filename: "sample_2054",
FilePath: "/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
URL: "https://example.com/metric",
URLText: "An example metric",
}
metricImage, resp, err := client.AlertManagement.UpdateMetricImage(1, 2, 17, &UpdateMetricImageOptions{
URL: Ptr("https://example.com/metric"),
URLText: Ptr("An example metric"),
})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, want, metricImage)
}
func TestAlertManagement_DeleteMetricImage(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/alert_management_alerts/2/metric_images/17", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodDelete)
})
resp, err := client.AlertManagement.DeleteMetricImage(1, 2, 17)
assert.NoError(t, err)
assert.NotNil(t, resp)
}
......@@ -18,16 +18,25 @@ package gitlab
import "net/http"
type (
AppearanceServiceInterface interface {
GetAppearance(options ...RequestOptionFunc) (*Appearance, *Response, error)
ChangeAppearance(opt *ChangeAppearanceOptions, options ...RequestOptionFunc) (*Appearance, *Response, error)
}
// AppearanceService handles communication with appearance of the Gitlab API.
//
// Gitlab API docs : https://docs.gitlab.com/ee/api/appearance.html
type AppearanceService struct {
// Gitlab API docs: https://docs.gitlab.com/api/appearance/
AppearanceService struct {
client *Client
}
)
var _ AppearanceServiceInterface = (*AppearanceService)(nil)
// Appearance represents a GitLab appearance.
//
// Gitlab API docs : https://docs.gitlab.com/ee/api/appearance.html
// Gitlab API docs: https://docs.gitlab.com/api/appearance/
type Appearance struct {
Title string `json:"title"`
Description string `json:"description"`
......@@ -51,7 +60,7 @@ type Appearance struct {
// GetAppearance gets the current appearance configuration of the GitLab instance.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/appearance.html#get-current-appearance-configuration
// https://docs.gitlab.com/api/appearance/#get-details-on-current-application-appearance
func (s *AppearanceService) GetAppearance(options ...RequestOptionFunc) (*Appearance, *Response, error) {
req, err := s.client.NewRequest(http.MethodGet, "application/appearance", nil, options)
if err != nil {
......@@ -70,7 +79,7 @@ func (s *AppearanceService) GetAppearance(options ...RequestOptionFunc) (*Appear
// ChangeAppearanceOptions represents the available ChangeAppearance() options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/appearance.html#change-appearance-configuration
// https://docs.gitlab.com/api/appearance/#update-application-appearance
type ChangeAppearanceOptions struct {
Title *string `url:"title,omitempty" json:"title,omitempty"`
Description *string `url:"description,omitempty" json:"description,omitempty"`
......@@ -95,7 +104,7 @@ type ChangeAppearanceOptions struct {
// ChangeAppearance changes the appearance configuration.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/appearance.html#change-appearance-configuration
// https://docs.gitlab.com/api/appearance/#update-application-appearance
func (s *AppearanceService) ChangeAppearance(opt *ChangeAppearanceOptions, options ...RequestOptionFunc) (*Appearance, *Response, error) {
req, err := s.client.NewRequest(http.MethodPut, "application/appearance", opt, options)
if err != nil {
......
......@@ -24,6 +24,7 @@ import (
)
func TestGetAppearance(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/application/appearance", func(w http.ResponseWriter, r *http.Request) {
......@@ -80,6 +81,7 @@ func TestGetAppearance(t *testing.T) {
}
func TestChangeAppearance(t *testing.T) {
t.Parallel()
mux, client := setup(t)
mux.HandleFunc("/api/v4/application/appearance", func(w http.ResponseWriter, r *http.Request) {
......
//
// 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 "net/http"
type (
ApplicationStatisticsServiceInterface interface {
GetApplicationStatistics(options ...RequestOptionFunc) (*ApplicationStatistics, *Response, error)
}
// ApplicationStatisticsService handles communication with the application
// statistics related methods of the GitLab API.
//
// GitLab API docs: https://docs.gitlab.com/api/statistics/
ApplicationStatisticsService struct {
client *Client
}
)
var _ ApplicationStatisticsServiceInterface = (*ApplicationStatisticsService)(nil)
// ApplicationStatistics represents application statistics.
//
// GitLab API docs: https://docs.gitlab.com/api/statistics/
type ApplicationStatistics struct {
Forks int `url:"forks" json:"forks"`
Issues int `url:"issues" json:"issues"`
MergeRequests int `url:"merge_requests" json:"merge_requests"`
Notes int `url:"notes" json:"notes"`
Snippets int `url:"snippets" json:"snippets"`
SSHKeys int `url:"ssh_keys" json:"ssh_keys"`
Milestones int `url:"milestones" json:"milestones"`
Users int `url:"users" json:"users"`
Groups int `url:"groups" json:"groups"`
Projects int `url:"projects" json:"projects"`
ActiveUsers int `url:"active_users" json:"active_users"`
}
// GetApplicationStatistics gets details on the current application statistics.
//
// GitLab API docs:
// https://docs.gitlab.com/api/statistics/#get-details-on-current-application-statistics
func (s *ApplicationStatisticsService) GetApplicationStatistics(options ...RequestOptionFunc) (*ApplicationStatistics, *Response, error) {
req, err := s.client.NewRequest(http.MethodGet, "application/statistics", nil, options)
if err != nil {
return nil, nil, err
}
statistics := new(ApplicationStatistics)
resp, err := s.client.Do(req, statistics)
if err != nil {
return nil, resp, err
}
return statistics, resp, nil
}