Commit 39e47fd5 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '292433-add-filtering-to-deploy-tokens-api' into 'master'

Add "active" filter to deploy tokens API

See merge request gitlab-org/gitlab!59582
parents 70f15d4f 0d60b072
# frozen_string_literal: true
# Arguments:
# current_user: The currently logged in user.
# scope: A Project or Group to scope deploy tokens to (or :all for all tokens).
# params:
# active: Boolean - When true, only return active deployments.
module DeployTokens
class TokensFinder
attr_reader :current_user, :params, :scope
def initialize(current_user, scope, params = {})
@current_user = current_user
@scope = scope
@params = params
end
def execute
by_active(init_collection)
end
private
def init_collection
case scope
when Group, Project
raise Gitlab::Access::AccessDeniedError unless current_user.can?(:read_deploy_token, scope)
scope.deploy_tokens
when :all
raise Gitlab::Access::AccessDeniedError unless current_user.can_read_all_resources?
DeployToken.all
else
raise ArgumentError, "Scope must be a Group, a Project, or the :all symbol."
end
end
def by_active(items)
params[:active] ? items.active : items
end
end
end
---
title: Add "active" filter to deploy tokens API
merge_request: 59582
author: Devin Christensen
type: added
...@@ -16,6 +16,12 @@ Get a list of all deploy tokens across the GitLab instance. This endpoint requir ...@@ -16,6 +16,12 @@ Get a list of all deploy tokens across the GitLab instance. This endpoint requir
GET /deploy_tokens GET /deploy_tokens
``` ```
Parameters:
| Attribute | Type | Required | Description |
|-----------|----------|------------------------|-------------|
| `active` | boolean | **{dotted-circle}** No | Limit by active status. |
Example request: Example request:
```shell ```shell
...@@ -31,6 +37,8 @@ Example response: ...@@ -31,6 +37,8 @@ Example response:
"name": "MyToken", "name": "MyToken",
"username": "gitlab+deploy-token-1", "username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z", "expires_at": "2020-02-14T00:00:00.000Z",
"revoked": false,
"expired": false,
"scopes": [ "scopes": [
"read_repository", "read_repository",
"read_registry" "read_registry"
...@@ -56,8 +64,9 @@ GET /projects/:id/deploy_tokens ...@@ -56,8 +64,9 @@ GET /projects/:id/deploy_tokens
Parameters: Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| |:---------------|:---------------|:-----------------------|:------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | | `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `active` | boolean | **{dotted-circle}** No | Limit by active status. |
Example request: Example request:
...@@ -74,6 +83,8 @@ Example response: ...@@ -74,6 +83,8 @@ Example response:
"name": "MyToken", "name": "MyToken",
"username": "gitlab+deploy-token-1", "username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z", "expires_at": "2020-02-14T00:00:00.000Z",
"revoked": false,
"expired": false,
"scopes": [ "scopes": [
"read_repository", "read_repository",
"read_registry" "read_registry"
...@@ -92,13 +103,17 @@ Creates a new deploy token for a project. ...@@ -92,13 +103,17 @@ Creates a new deploy token for a project.
POST /projects/:id/deploy_tokens POST /projects/:id/deploy_tokens
``` ```
Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | ------------ | ---------------- | ---------------------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | New deploy token's name | | `name` | string | **{check-circle}** Yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | | `expires_at` | datetime | **{dotted-circle}** No | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` | | `username` | string | **{dotted-circle}** No | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. | | `scopes` | array of strings | **{check-circle}** Yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Example request:
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "My deploy token", "expires_at": "2021-01-01", "username": "custom-user", "scopes": ["read_repository"]}' "https://gitlab.example.com/api/v4/projects/5/deploy_tokens/" curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "My deploy token", "expires_at": "2021-01-01", "username": "custom-user", "scopes": ["read_repository"]}' "https://gitlab.example.com/api/v4/projects/5/deploy_tokens/"
...@@ -113,6 +128,8 @@ Example response: ...@@ -113,6 +128,8 @@ Example response:
"username": "custom-user", "username": "custom-user",
"expires_at": "2021-01-01T00:00:00.000Z", "expires_at": "2021-01-01T00:00:00.000Z",
"token": "jMRvtPNxrn3crTAGukpZ", "token": "jMRvtPNxrn3crTAGukpZ",
"revoked": false,
"expired": false,
"scopes": [ "scopes": [
"read_repository" "read_repository"
] ]
...@@ -129,10 +146,12 @@ Removes a deploy token from the project. ...@@ -129,10 +146,12 @@ Removes a deploy token from the project.
DELETE /projects/:id/deploy_tokens/:token_id DELETE /projects/:id/deploy_tokens/:token_id
``` ```
Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | ---------- | -------------- | ---------------------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `token_id` | integer | yes | The ID of the deploy token | | `token_id` | integer | **{check-circle}** Yes | The ID of the deploy token |
Example request: Example request:
...@@ -158,8 +177,9 @@ GET /groups/:id/deploy_tokens ...@@ -158,8 +177,9 @@ GET /groups/:id/deploy_tokens
Parameters: Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| |:---------------|:---------------|:-----------------------|:------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | | `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `active` | boolean | **{dotted-circle}** No | Limit by active status. |
Example request: Example request:
...@@ -176,6 +196,8 @@ Example response: ...@@ -176,6 +196,8 @@ Example response:
"name": "MyToken", "name": "MyToken",
"username": "gitlab+deploy-token-1", "username": "gitlab+deploy-token-1",
"expires_at": "2020-02-14T00:00:00.000Z", "expires_at": "2020-02-14T00:00:00.000Z",
"revoked": false,
"expired": false,
"scopes": [ "scopes": [
"read_repository", "read_repository",
"read_registry" "read_registry"
...@@ -194,13 +216,15 @@ Creates a new deploy token for a group. ...@@ -194,13 +216,15 @@ Creates a new deploy token for a group.
POST /groups/:id/deploy_tokens POST /groups/:id/deploy_tokens
``` ```
Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | ------------ | ---- | --------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | New deploy token's name | | `name` | string | **{check-circle}** Yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) | | `expires_at` | datetime | **{dotted-circle}** No | Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`) |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` | | `username` | string | **{dotted-circle}** No | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. | | `scopes` | array of strings | **{check-circle}** Yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Example request: Example request:
...@@ -217,6 +241,8 @@ Example response: ...@@ -217,6 +241,8 @@ Example response:
"username": "custom-user", "username": "custom-user",
"expires_at": "2021-01-01T00:00:00.000Z", "expires_at": "2021-01-01T00:00:00.000Z",
"token": "jMRvtPNxrn3crTAGukpZ", "token": "jMRvtPNxrn3crTAGukpZ",
"revoked": false,
"expired": false,
"scopes": [ "scopes": [
"read_registry" "read_registry"
] ]
...@@ -233,10 +259,12 @@ Removes a deploy token from the group. ...@@ -233,10 +259,12 @@ Removes a deploy token from the group.
DELETE /groups/:id/deploy_tokens/:token_id DELETE /groups/:id/deploy_tokens/:token_id
``` ```
Parameters:
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | ----------- | -------------- | ---------------------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `token_id` | integer | yes | The ID of the deploy token | | `token_id` | integer | **{check-circle}** Yes | The ID of the deploy token |
Example request: Example request:
......
...@@ -18,6 +18,10 @@ module API ...@@ -18,6 +18,10 @@ module API
result_hash[:read_repository] = scopes.include?('read_repository') result_hash[:read_repository] = scopes.include?('read_repository')
result_hash result_hash
end end
params :filter_params do
optional :active, type: Boolean, desc: 'Limit by active status'
end
end end
desc 'Return all deploy tokens' do desc 'Return all deploy tokens' do
...@@ -26,11 +30,18 @@ module API ...@@ -26,11 +30,18 @@ module API
end end
params do params do
use :pagination use :pagination
use :filter_params
end end
get 'deploy_tokens' do get 'deploy_tokens' do
authenticated_as_admin! authenticated_as_admin!
present paginate(DeployToken.all), with: Entities::DeployToken deploy_tokens = ::DeployTokens::TokensFinder.new(
current_user,
:all,
declared_params
).execute
present paginate(deploy_tokens), with: Entities::DeployToken
end end
params do params do
...@@ -39,6 +50,7 @@ module API ...@@ -39,6 +50,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do params do
use :pagination use :pagination
use :filter_params
end end
desc 'List deploy tokens for a project' do desc 'List deploy tokens for a project' do
detail 'This feature was introduced in GitLab 12.9' detail 'This feature was introduced in GitLab 12.9'
...@@ -47,7 +59,13 @@ module API ...@@ -47,7 +59,13 @@ module API
get ':id/deploy_tokens' do get ':id/deploy_tokens' do
authorize!(:read_deploy_token, user_project) authorize!(:read_deploy_token, user_project)
present paginate(user_project.deploy_tokens), with: Entities::DeployToken deploy_tokens = ::DeployTokens::TokensFinder.new(
current_user,
user_project,
declared_params
).execute
present paginate(deploy_tokens), with: Entities::DeployToken
end end
params do params do
...@@ -98,6 +116,7 @@ module API ...@@ -98,6 +116,7 @@ module API
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do params do
use :pagination use :pagination
use :filter_params
end end
desc 'List deploy tokens for a group' do desc 'List deploy tokens for a group' do
detail 'This feature was introduced in GitLab 12.9' detail 'This feature was introduced in GitLab 12.9'
...@@ -106,7 +125,13 @@ module API ...@@ -106,7 +125,13 @@ module API
get ':id/deploy_tokens' do get ':id/deploy_tokens' do
authorize!(:read_deploy_token, user_group) authorize!(:read_deploy_token, user_group)
present paginate(user_group.deploy_tokens), with: Entities::DeployToken deploy_tokens = ::DeployTokens::TokensFinder.new(
current_user,
user_group,
declared_params
).execute
present paginate(deploy_tokens), with: Entities::DeployToken
end end
params do params do
......
...@@ -4,7 +4,8 @@ module API ...@@ -4,7 +4,8 @@ module API
module Entities module Entities
class DeployToken < Grape::Entity class DeployToken < Grape::Entity
# exposing :token is a security risk and should be avoided # exposing :token is a security risk and should be avoided
expose :id, :name, :username, :expires_at, :scopes expose :id, :name, :username, :expires_at, :scopes, :revoked
expose :expired?, as: :expired
end end
end end
end end
...@@ -59,6 +59,8 @@ RSpec.describe Groups::Settings::RepositoryController do ...@@ -59,6 +59,8 @@ RSpec.describe Groups::Settings::RepositoryController do
'username' => deploy_token_params[:username], 'username' => deploy_token_params[:username],
'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]), 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
'token' => be_a(String), 'token' => be_a(String),
'expired' => false,
'revoked' => false,
'scopes' => deploy_token_params.inject([]) do |scopes, kv| 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv key, value = kv
key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
......
...@@ -78,6 +78,8 @@ RSpec.describe Projects::Settings::RepositoryController do ...@@ -78,6 +78,8 @@ RSpec.describe Projects::Settings::RepositoryController do
'username' => deploy_token_params[:username], 'username' => deploy_token_params[:username],
'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]), 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
'token' => be_a(String), 'token' => be_a(String),
'expired' => false,
'revoked' => false,
'scopes' => deploy_token_params.inject([]) do |scopes, kv| 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv key, value = kv
key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DeployTokens::TokensFinder do
include AdminModeHelper
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:project) { create(:project, creator_id: user.id) }
let_it_be(:group) { create(:group) }
let!(:project_deploy_token) { create(:deploy_token, projects: [project]) }
let!(:revoked_project_deploy_token) { create(:deploy_token, projects: [project], revoked: true) }
let!(:expired_project_deploy_token) { create(:deploy_token, projects: [project], expires_at: '1988-01-11T04:33:04-0600') }
let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
let!(:revoked_group_deploy_token) { create(:deploy_token, :group, groups: [group], revoked: true) }
let!(:expired_group_deploy_token) { create(:deploy_token, :group, groups: [group], expires_at: '1988-01-11T04:33:04-0600') }
describe "#execute" do
let(:params) { {} }
context 'when scope is :all' do
subject { described_class.new(admin, :all, params).execute }
before do
enable_admin_mode!(admin)
end
it 'returns all deploy tokens' do
expect(subject.size).to eq(6)
is_expected.to match_array([
project_deploy_token,
revoked_project_deploy_token,
expired_project_deploy_token,
group_deploy_token,
revoked_group_deploy_token,
expired_group_deploy_token
])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens' do
is_expected.to match_array([
project_deploy_token,
group_deploy_token
])
end
end
context 'but user is not an admin' do
subject { described_class.new(user, :all, params).execute }
it 'raises Gitlab::Access::AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'when scope is a Project' do
subject { described_class.new(user, project, params).execute }
before do
project.add_maintainer(user)
end
it 'returns all deploy tokens for the project' do
is_expected.to match_array([
project_deploy_token,
revoked_project_deploy_token,
expired_project_deploy_token
])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens for the project' do
is_expected.to match_array([project_deploy_token])
end
end
context 'but user is not a member' do
subject { described_class.new(other_user, :all, params).execute }
it 'raises Gitlab::Access::AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'when scope is a Group' do
subject { described_class.new(user, group, params).execute }
before do
group.add_maintainer(user)
end
it 'returns all deploy tokens for the group' do
is_expected.to match_array([
group_deploy_token,
revoked_group_deploy_token,
expired_group_deploy_token
])
end
context 'and active filter is applied' do
let(:params) { { active: true } }
it 'returns only active tokens for the group' do
is_expected.to match_array([group_deploy_token])
end
end
context 'but user is not a member' do
subject { described_class.new(other_user, :all, params).execute }
it 'raises Gitlab::Access::AccessDeniedError' do
expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
context 'when scope is nil' do
subject { described_class.new(user, nil, params).execute }
it 'raises ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
end
end
end
end
...@@ -8,7 +8,11 @@ RSpec.describe API::DeployTokens do ...@@ -8,7 +8,11 @@ RSpec.describe API::DeployTokens do
let_it_be(:project) { create(:project, creator_id: creator.id) } let_it_be(:project) { create(:project, creator_id: creator.id) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let!(:deploy_token) { create(:deploy_token, projects: [project]) } let!(:deploy_token) { create(:deploy_token, projects: [project]) }
let!(:revoked_deploy_token) { create(:deploy_token, projects: [project], revoked: true) }
let!(:expired_deploy_token) { create(:deploy_token, projects: [project], expires_at: '1988-01-11T04:33:04-0600') }
let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) } let!(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
let!(:revoked_group_deploy_token) { create(:deploy_token, :group, groups: [group], revoked: true) }
let!(:expired_group_deploy_token) { create(:deploy_token, :group, groups: [group], expires_at: '1988-01-11T04:33:04-0600') }
describe 'GET /deploy_tokens' do describe 'GET /deploy_tokens' do
subject do subject do
...@@ -36,8 +40,31 @@ RSpec.describe API::DeployTokens do ...@@ -36,8 +40,31 @@ RSpec.describe API::DeployTokens do
it 'returns all deploy tokens' do it 'returns all deploy tokens' do
subject subject
token_ids = json_response.map { |token| token['id'] }
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/deploy_tokens') expect(response).to match_response_schema('public_api/v4/deploy_tokens')
expect(token_ids).to match_array([
deploy_token.id,
revoked_deploy_token.id,
expired_deploy_token.id,
group_deploy_token.id,
revoked_group_deploy_token.id,
expired_group_deploy_token.id
])
end
context 'and active=true' do
it 'only returns active deploy tokens' do
get api('/deploy_tokens?active=true', user)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(token_ids).to match_array([
deploy_token.id,
group_deploy_token.id
])
end
end end
end end
end end
...@@ -82,7 +109,22 @@ RSpec.describe API::DeployTokens do ...@@ -82,7 +109,22 @@ RSpec.describe API::DeployTokens do
subject subject
token_ids = json_response.map { |token| token['id'] } token_ids = json_response.map { |token| token['id'] }
expect(token_ids).not_to include(other_deploy_token.id) expect(token_ids).to match_array([
deploy_token.id,
expired_deploy_token.id,
revoked_deploy_token.id
])
end
context 'and active=true' do
it 'only returns active deploy tokens for the project' do
get api("/projects/#{project.id}/deploy_tokens?active=true", user)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(token_ids).to match_array([deploy_token.id])
end
end end
end end
end end
...@@ -119,8 +161,10 @@ RSpec.describe API::DeployTokens do ...@@ -119,8 +161,10 @@ RSpec.describe API::DeployTokens do
it 'returns all deploy tokens for the group' do it 'returns all deploy tokens for the group' do
subject subject
token_ids = json_response.map { |token| token['id'] }
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/deploy_tokens') expect(response).to match_response_schema('public_api/v4/deploy_tokens')
expect(token_ids.length).to be(3)
end end
it 'does not return deploy tokens for other groups' do it 'does not return deploy tokens for other groups' do
...@@ -129,6 +173,17 @@ RSpec.describe API::DeployTokens do ...@@ -129,6 +173,17 @@ RSpec.describe API::DeployTokens do
token_ids = json_response.map { |token| token['id'] } token_ids = json_response.map { |token| token['id'] }
expect(token_ids).not_to include(other_deploy_token.id) expect(token_ids).not_to include(other_deploy_token.id)
end end
context 'and active=true' do
it 'only returns active deploy tokens for the group' do
get api("/groups/#{group.id}/deploy_tokens?active=true", user)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(token_ids).to eql([group_deploy_token.id])
end
end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment