Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-06 03:07:35 +00:00
parent bd9860f691
commit bdb97ece68
27 changed files with 275 additions and 318 deletions

View File

@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NotebookLab from '~/notebook/index.vue';
export default {
@ -51,7 +52,7 @@ export default {
this.loading = false;
})
.catch((e) => {
if (e.status !== 200) {
if (e.status !== HTTP_STATUS_OK) {
this.loadError = true;
}
this.error = true;

View File

@ -1,3 +1,4 @@
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getWebSocketUrl, mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import createDiff from './create_diff';
@ -26,7 +27,7 @@ const cancellableWait = (time) => {
const isErrorResponse = (error) => error && error.code !== 0;
const isErrorPayload = (payload) => payload && payload.status_code !== 200;
const isErrorPayload = (payload) => payload && payload.status_code !== HTTP_STATUS_OK;
const getErrorFromResponse = (data) => {
if (isErrorResponse(data.error)) {

View File

@ -58,7 +58,7 @@ module Clusters
if project_entries
allowed_projects.where_full_path_in(project_entries.keys).map do |project|
{ project_id: project.id, config: project_entries[project.full_path] }
{ project_id: project.id, config: project_entries[project.full_path.downcase] }
end
end
end
@ -70,7 +70,7 @@ module Clusters
if group_entries
allowed_groups.where_full_path_in(group_entries.keys).map do |group|
{ group_id: group.id, config: group_entries[group.full_path] }
{ group_id: group.id, config: group_entries[group.full_path.downcase] }
end
end
end
@ -79,7 +79,7 @@ module Clusters
def extract_config_entries(entity:)
config.dig('ci_access', entity)
&.first(AUTHORIZED_ENTITY_LIMIT)
&.index_by { |config| config.delete('id') }
&.index_by { |config| config.delete('id').downcase }
end
def allowed_projects

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
class RetryBackfillTraversalIds < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
ROOTS_MIGRATION = 'BackfillNamespaceTraversalIdsRoots'
CHILDREN_MIGRATION = 'BackfillNamespaceTraversalIdsChildren'
DOWNTIME = false
DELAY_INTERVAL = 2.minutes
disable_ddl_transaction!
def up
duration = requeue_background_migration_jobs_by_range_at_intervals(ROOTS_MIGRATION, DELAY_INTERVAL)
requeue_background_migration_jobs_by_range_at_intervals(CHILDREN_MIGRATION, DELAY_INTERVAL, initial_delay: duration)
end
def down
# no-op
end
end

View File

@ -1 +0,0 @@
ec44b7f134de2ea6537c6fe3109fa9d7e32785233f3d1b8e9ea118474d21526a

View File

@ -378,6 +378,7 @@ Component statuses are linked to configuration documentation for each component.
| [Runner](#gitlab-runner) | Executes GitLab CI/CD jobs | ⤓ | ⤓ | ✅ | ⚙ | ✅ | ⚙ | ⚙ | CE & EE |
| [Sentry integration](#sentry) | Error tracking for deployed apps | ⤓ | ⤓ | ⤓ | ⤓ | ⤓ | ⤓ | ⤓ | CE & EE |
| [Sidekiq](#sidekiq) | Background jobs processor | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | CE & EE |
| [Token Revocation API](sec/token_revocation_api.md) | Receives and revokes leaked secrets | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | EE Only |
### Component details

View File

@ -0,0 +1,118 @@
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Token Revocation API
The Token Revocation API is an externally-deployed HTTP API that interfaces with GitLab
to receive and revoke API tokens and other secrets detected by GitLab Secret Detection.
See the [high-level architecture](../../user/application_security/secret_detection/post_processing.md)
to understand the Secret Detection post-processing and revocation flow.
GitLab.com uses the internally-maintained [Secret Revocation Service](https://gitlab.com/gitlab-com/gl-security/engineering-and-research/automation-team/secret-revocation-service)
(team-members only) as its Token Revocation API. For GitLab self-managed, you can create
your own API and configure GitLab to use it.
## Implement a Token Revocation API for self-managed
GitLab self-managed instances interested in using the revocation capabilities must:
- Implement and deploy your own Token Revocation API.
- Configure the GitLab instance to use the Token Revocation API.
Your service must:
- Match the API specification below.
- Provide two endpoints:
- Fetching revocable token types.
- Revoking leaked tokens.
- Be rate-limited and idempotent.
Requests to the documented endpoints are authenticated using API tokens passed in
the `Authorization` header. Request and response bodies, if present, are
expected to have the content type `application/json`.
All endpoints may return these responses:
- `401 Unauthorized`
- `405 Method Not Allowed`
- `500 Internal Server Error`
### `GET /v1/revocable_token_types`
Returns the valid `type` values for use in the `revoke_tokens` endpoint.
NOTE:
These values match the concatenation of [the `secrets` analyzer's](../../user/application_security/secret_detection/index.md)
[primary identifier](../integrations/secure.md#identifiers) by means
of concatenating the `primary_identifier.type` and `primary_identifier.value`.
For example, the value `gitleaks_rule_id_gitlab_personal_access_token` matches the following finding identifier:
```json
{"type": "gitleaks_rule_id", "name": "Gitleaks rule ID GitLab Personal Access Token", "value": "GitLab Personal Access Token"}
```
| Status Code | Description |
| ----- | --- |
| `200` | The response body contains the valid token `type` values. |
Example response body:
```json
{
"types": ["gitleaks_rule_id_gitlab_personal_access_token"]
}
```
### `POST /v1/revoke_tokens`
Accepts a list of tokens to be revoked by the appropriate provider. Your service is responsible for communicating
with each provider to revoke the token.
| Status Code | Description |
| ----- | --- |
| `204` | All submitted tokens have been accepted for eventual revocation. |
| `400` | The request body is invalid or one of the submitted token types is not supported. The request should not be retried. |
| `429` | The provider has received too many requests. The request should be retried later. |
Example request body:
```json
[{
"type": "gitleaks_rule_id_gitlab_personal_access_token",
"token": "glpat--8GMtG8Mf4EnMJzmAWDU",
"location": "https://example.com/some-repo/blob/abcdefghijklmnop/compromisedfile1.java"
},
{
"type": "gitleaks_rule_id_gitlab_personal_access_token",
"token": "glpat--tG84EGK33nMLLDE70zU",
"location": "https://example.com/some-repo/blob/abcdefghijklmnop/compromisedfile2.java"
}]
```
### Configure GitLab to interface with the Token Revocation API
You must configure the following database settings in the GitLab instance:
| Setting | Type | Description |
| ------- | ---- | ----------- |
| `secret_detection_token_revocation_enabled` | Boolean | Whether automatic token revocation is enabled |
| `secret_detection_token_revocation_url` | String | A fully-qualified URL to the `/v1/revoke_tokens` endpoint of the Token Revocation API |
| `secret_detection_revocation_token_types_url` | String | A fully-qualified URL to the `/v1/revocable_token_types` endpoint of the Token Revocation API |
| `secret_detection_token_revocation_token` | String | A pre-shared token to authenticate requests to the Token Revocation API |
For example, to configure these values in the
[Rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session):
```ruby
::Gitlab::CurrentSettings.update!(secret_detection_token_revocation_token: 'MYSECRETTOKEN')
::Gitlab::CurrentSettings.update!(secret_detection_token_revocation_url: 'https://example.gitlab.com/revocation_service/v1/revoke_tokens')
::Gitlab::CurrentSettings.update!(secret_detection_revocation_token_types_url: 'https://example.gitlab.com/revocation_service/v1/revocable_token_types')
::Gitlab::CurrentSettings.update!(secret_detection_token_revocation_enabled: true)
```
After you configure these values, the Token Revocation API will be called according to the
[high-level architecture](../../user/application_security/secret_detection/post_processing.md#high-level-architecture)
diagram.

View File

@ -195,7 +195,12 @@ Pipelines now include a Secret Detection job.
## Responding to a leaked secret
If the scanner detects a secret you should rotate it immediately. [Purging a file from the repository's history](../../project/repository/reducing_the_repo_size_using_git.md#purge-files-from-repository-history) may not be effective in removing all references to the file. Also, the secret remains in any forks of the repository.
Secrets detected by the analyzer should be immediately rotated.
[Purging a file from the repository's history](../../project/repository/reducing_the_repo_size_using_git.md#purge-files-from-repository-history)
may not be effective in removing all references to the file. Additionally, the secret will remain in any existing
forks or clones of the repository.
GitLab will attempt to [automatically revoke](post_processing.md) some types of leaked secrets.
## Pinning to specific analyzer version

View File

@ -10,26 +10,28 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Disabled by default for GitLab personal access tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/371658) in GitLab 15.6 [with a flag](../../../administration/feature_flags.md) named `gitlab_pat_auto_revocation`. Available to GitLab.com only.
> - [Enabled by default for GitLab personal access tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/371658) in GitLab 15.9
GitLab supports running post-processing hooks after detecting a secret. These
hooks can perform actions, like notifying the cloud service that issued the secret.
The cloud provider can then confirm the credentials and take remediation actions, like:
GitLab.com and self-managed supports running post-processing hooks after detecting a secret. These
hooks can perform actions, like notifying the vendor that issued the secret.
The vendor can then confirm the credentials and take remediation actions, like:
- Revoking a secret.
- Reissuing a secret.
- Notifying the creator of the secret.
GitLab SaaS supports post-processing for [GitLab personal access tokens](../../profile/personal_access_tokens.md) and Amazon Web Services (AWS).
Post-processing workflows vary by supported cloud providers.
GitLab supports post-processing for the following vendors and secrets:
Post-processing is limited to a project's default branch. The epic
[Post-processing of leaked secrets](https://gitlab.com/groups/gitlab-org/-/epics/4639).
contains:
| Vendor | Secret | GitLab.com | Self-managed |
| ----- | --- | --- | --- |
| GitLab | [Personal access tokens](../../profile/personal_access_tokens.md) | ✅ | ✅ 15.9 and later |
| Amazon Web Services (AWS) | [IAM access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) | ✅ | ⚙ |
- Technical details of post-processing secrets.
- Discussions of efforts to support additional branches.
**Component legend**
- ✅ - Available by default
- ⚙ - Requires manual integration using a [Token Revocation API](../../../development/sec/token_revocation_api.md)
NOTE:
Post-processing is currently limited to a project's default branch
Post-processing is limited to a project's default branch.
## High-level architecture
@ -40,142 +42,51 @@ sequenceDiagram
autonumber
GitLab Rails->>+Sidekiq: gl-secret-detection-report.json
Sidekiq-->+Sidekiq: StoreSecurityReportsWorker
Sidekiq-->+RevocationAPI: GET revocable keys types
RevocationAPI-->>-Sidekiq: OK
Sidekiq->>+RevocationAPI: POST revoke revocable keys
RevocationAPI-->>-Sidekiq: ACCEPTED
RevocationAPI-->>+Cloud Vendor: revoke revocable keys
Cloud Vendor-->>+RevocationAPI: ACCEPTED
Sidekiq-->+Token Revocation API: GET revocable keys types
Token Revocation API-->>-Sidekiq: OK
Sidekiq->>+Token Revocation API: POST revoke revocable keys
Token Revocation API-->>-Sidekiq: ACCEPTED
Token Revocation API-->>+Receiver Service: revoke revocable keys
Receiver Service-->>+Token Revocation API: ACCEPTED
```
## Integrate your cloud provider service with GitLab SaaS
1. A pipeline with a Secret Detection job completes on the project's default branch, producing a scan
report (**1**).
1. The report is processed (**2**) by an asynchronous worker, which communicates with an externally
deployed HTTP service (**3** and **4**) to determine which kinds of secrets can be automatically
revoked.
1. The worker sends (**5** and **6**) the list of detected secrets which the Token Revocation API is able to
revoke.
1. The Token Revocation API sends (**7** and **8**) each revocable token to their respective vendor's [receiver service](#integrate-your-cloud-provider-service-with-gitlabcom).
Third party cloud and SaaS providers can [express integration interest by filling out this form](https://forms.gle/wWpvrtLRK21Q2WJL9).
See the [Token Revocation API](../../../development/sec/token_revocation_api.md) documentation for more
information.
### Implement a vendor revocation receiver service
## Integrate your cloud provider service with GitLab.com
A vendor revocation receiver service integrates with a GitLab instance to receive
a web notification and respond to leaked token requests.
Third-party cloud and SaaS vendors interested in automated token revocation can
[express integration interest by filling out this form](https://forms.gle/wWpvrtLRK21Q2WJL9).
Vendors must [implement a revocation receiver service](#implement-a-revocation-receiver-service)
which will be called by the Token Revocation API.
To implement a receiver service to revoke leaked tokens:
### Implement a revocation receiver service
1. Create a publicly accessible HTTP service matching the corresponding API contract
below. Your service should be idempotent and rate-limited.
1. When a pipeline corresponding to its revocable token type (in the example, `my_api_token`)
completes, GitLab sends a request to your receiver service.
1. The included URL should be publicly accessible, and contain the commit where the
leaked token can be found. For example:
A revocation receiver service integrates with a GitLab instance's Token Revocation API to receive and respond
to leaked token revocation requests. The service should be a publicly accessible HTTP API that is
idempotent and rate-limited. Requests to your service from the Token Revocation API will follow the example
below:
```plaintext
POST / HTTP/2
Accept: */*
Content-Type: application/json
X-Gitlab-Token: MYSECRETTOKEN
```plaintext
POST / HTTP/2
Accept: */*
Content-Type: application/json
X-Gitlab-Token: MYSECRETTOKEN
[
{"type": "my_api_token", "token":"XXXXXXXXXXXXXXXX","url": "https://example.com/some-repo/blob/abcdefghijklmnop/compromisedfile1.java"}
]
```
## Implement a revocation service for self-managed
Self-managed instances interested in using the revocation capabilities must:
- Deploy the [RevocationAPI](#high-level-architecture).
- Configure the GitLab instance to use the RevocationAPI.
A RevocationAPI must:
- Match a minimal API specification.
- Provide two endpoints:
- Fetching revocable token types.
- Revoking leaked tokens.
- Be rate-limited and idempotent.
Requests to the documented endpoints are authenticated via API tokens passed in
the `Authorization` header. Request and response bodies, if present, are
expected to have the content type `application/json`.
All endpoints may return these responses:
- `401 Unauthorized`
- `405 Method Not Allowed`
- `500 Internal Server Error`
### `GET /v1/revocable_token_types`
Returns the valid `type` values for use in the `revoke_tokens` endpoint.
NOTE:
These values match the concatenation of [the `secrets` analyzer's](index.md)
[primary identifier](../../../development/integrations/secure.md#identifiers) by means
of concatenating the `primary_identifier.type` and `primary_identifier.value`.
In the case below, a finding identifier matches:
```json
{"type": "gitleaks_rule_id", "name": "Gitleaks rule ID GitLab Personal Access Token", "value": "GitLab Personal Access Token"}
[
{"type": "my_api_token", "token":"XXXXXXXXXXXXXXXX","url": "https://example.com/some-repo/~/raw/abcdefghijklmnop/compromisedfile1.java"}
]
```
| Status Code | Description |
| ----- | --- |
| `200` | The response body contains the valid token `type` values. |
Example response body:
```json
{
"types": ["gitleaks_rule_id_gitlab_personal_access_token"]
}
```
### `POST /v1/revoke_tokens`
Accepts a list of tokens to be revoked by the appropriate provider.
| Status Code | Description |
| ----- | --- |
| `204` | All submitted tokens have been accepted for eventual revocation. |
| `400` | The request body is invalid or one of the submitted token types is not supported. The request should not be retried. |
| `429` | The provider has received too many requests. The request should be retried later. |
Example request body:
```json
[{
"type": "gitleaks_rule_id_gitlab_personal_access_token",
"token": "glpat--8GMtG8Mf4EnMJzmAWDU",
"location": "https://example.com/some-repo/blob/abcdefghijklmnop/compromisedfile1.java"
},
{
"type": "gitleaks_rule_id_gitlab_personal_access_token",
"token": "glpat--tG84EGK33nMLLDE70zU",
"location": "https://example.com/some-repo/blob/abcdefghijklmnop/compromisedfile2.java"
}]
```
### Configure GitLab to interface with RevocationAPI
You must configure the following database settings in the GitLab instance:
- `secret_detection_token_revocation_enabled`
- `secret_detection_token_revocation_url`
- `secret_detection_token_revocation_token`
- `secret_detection_revocation_token_types_url`
For example, to configure these values in the
[Rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session):
```ruby
::Gitlab::CurrentSettings.update!(secret_detection_token_revocation_token: 'MYSECRETTOKEN')
::Gitlab::CurrentSettings.update!(secret_detection_token_revocation_url: 'https://example.gitlab.com/revocation_service/v1/revoke_tokens')
::Gitlab::CurrentSettings.update!(secret_detection_revocation_token_types_url: 'https://example.gitlab.com/revocation_service/v1/revocable_token_types')
::Gitlab::CurrentSettings.update!(secret_detection_token_revocation_enabled: true)
```
After you configure these values, completing a pipeline performs these actions:
1. The revocation service is triggered once.
1. A request is made to `secret_detection_revocation_token_types_url` to fetch a
list of revocable tokens.
1. Any Secret Detection findings matching the results of the `token_types` request
are included in the subsequent revocation request.
In this example, Secret Detection has determined that an instance of `my_api_token` has been leaked. The
value of the token is provided to you, in addition to a publicly accessible URL to the raw content of the
file containing the leaked token.

View File

@ -5,7 +5,7 @@ import actions, { transformBackendBadge } from '~/badges/store/actions';
import mutationTypes from '~/badges/store/mutation_types';
import createState from '~/badges/store/state';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge';
describe('Badges store actions', () => {
@ -99,7 +99,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
dispatch.mockClear();
return [200, dummyResponse];
return [HTTP_STATUS_OK, dummyResponse];
});
const dummyBadge = transformBackendBadge(dummyResponse);
@ -177,7 +177,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
return [200, ''];
return [HTTP_STATUS_OK, ''];
});
await actions.deleteBadge({ state, dispatch }, { id: badgeId });
@ -266,7 +266,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
dispatch.mockClear();
return [200, dummyReponse];
return [HTTP_STATUS_OK, dummyReponse];
});
await actions.loadBadges({ state, dispatch }, dummyData);
@ -381,7 +381,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
dispatch.mockClear();
return [200, dummyReponse];
return [HTTP_STATUS_OK, dummyReponse];
});
await actions.renderBadge({ state, dispatch });
@ -468,7 +468,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
dispatch.mockClear();
return [200, dummyResponse];
return [HTTP_STATUS_OK, dummyResponse];
});
const updatedBadge = transformBackendBadge(dummyResponse);

View File

@ -1,6 +1,7 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
OPERATORS_IS,
OPERATORS_IS_NOT,
@ -460,7 +461,7 @@ export const BoardsMockData = {
export const boardsMockInterceptor = (config) => {
const body = BoardsMockData[config.method.toUpperCase()][config.url];
return [200, body];
return [HTTP_STATUS_OK, body];
};
export const mockList = {

View File

@ -7,6 +7,7 @@ import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { apiData } from '../mock_data';
describe('Clusters', () => {
@ -68,7 +69,7 @@ describe('Clusters', () => {
captureException = jest.spyOn(Sentry, 'captureException');
mock = new MockAdapter(axios);
mockPollingApi(200, apiData, paginationHeader());
mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader());
return createWrapper({});
});
@ -255,7 +256,7 @@ describe('Clusters', () => {
const totalSecondPage = 500;
beforeEach(() => {
mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1));
mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalFirstPage, perPage, 1));
return createWrapper({});
});
@ -269,7 +270,7 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2));
mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalSecondPage, perPage, 2));
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentPage: 2 });

View File

@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
@ -68,7 +69,7 @@ describe('~/environments/components/edit.vue', () => {
expect(showsLoading()).toBe(false);
await submitForm(expected, [200, { path: '/test' }]);
await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
@ -76,7 +77,7 @@ describe('~/environments/components/edit.vue', () => {
it('submits the updated environment on submit', async () => {
const expected = { url: 'https://google.ca' };
await submitForm(expected, [200, { path: '/test' }]);
await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});

View File

@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
@ -79,7 +80,7 @@ describe('~/environments/components/new.vue', () => {
expect(showsLoading()).toBe(false);
await submitForm(expected, [200, { path: '/test' }]);
await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
@ -87,7 +88,7 @@ describe('~/environments/components/new.vue', () => {
it('submits the new environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await submitForm(expected, [200, { path: '/test' }]);
await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});

View File

@ -29,7 +29,7 @@ describe('error tracking settings actions', () => {
});
it('should request and transform the project list', async () => {
mock.onGet(TEST_HOST).reply(() => [200, { projects: projectList }]);
mock.onGet(TEST_HOST).reply(() => [HTTP_STATUS_OK, { projects: projectList }]);
await testAction(
actions.fetchProjects,
null,

View File

@ -7,7 +7,7 @@ import {
MSG_CONNECTION_ERROR,
SERVICE_DELAY,
} from '~/ide/lib/mirror';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getWebSocketUrl } from '~/lib/utils/url_utility';
jest.mock('~/ide/lib/create_diff', () => jest.fn());
@ -19,10 +19,13 @@ const TEST_DIFF = {
};
const TEST_ERROR = 'Something bad happened...';
const TEST_SUCCESS_RESPONSE = {
data: JSON.stringify({ error: { code: 0 }, payload: { status_code: 200 } }),
data: JSON.stringify({ error: { code: 0 }, payload: { status_code: HTTP_STATUS_OK } }),
};
const TEST_ERROR_RESPONSE = {
data: JSON.stringify({ error: { code: 1, Message: TEST_ERROR }, payload: { status_code: 200 } }),
data: JSON.stringify({
error: { code: 1, Message: TEST_ERROR },
payload: { status_code: HTTP_STATUS_OK },
}),
};
const TEST_ERROR_PAYLOAD_RESPONSE = {
data: JSON.stringify({

View File

@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import * as terminalService from '~/ide/services/terminals';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/ipsum/dolar';
const TEST_BRANCH = 'ref';
@ -11,7 +12,7 @@ describe('~/ide/services/terminals', () => {
const prevRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
axiosSpy = jest.fn().mockReturnValue([200, {}]);
axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]);
mock = new MockAdapter(axios);
mock.onPost(/.*/).reply((...args) => axiosSpy(...args));

View File

@ -23,6 +23,7 @@ import PinnedLinks from '~/issues/show/components/pinned_links.vue';
import { POLLING_DELAY } from '~/issues/show/constants';
import eventHub from '~/issues/show/event_hub';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import {
appProps,
@ -100,7 +101,7 @@ describe('Issuable output', () => {
mock
.onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
.reply(() => {
const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
realtimeRequestCount += 1;
return res;
});
@ -336,7 +337,9 @@ describe('Issuable output', () => {
const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
mock
.onGet('/issuable-templates-path')
.reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
@ -345,7 +348,9 @@ describe('Issuable output', () => {
it('shows the form if template names as array request is successful', () => {
const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
mock
.onGet('/issuable-templates-path')
.reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);

View File

@ -1,5 +1,6 @@
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
beforeAll(async () => {
// @rails/ujs expects jQuery.ajaxPrefilter to exist if jQuery exists at
@ -20,7 +21,7 @@ function mockXHRResponse({ responseText, responseContentType } = {}) {
jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() {
Object.defineProperties(this, {
readyState: { value: XMLHttpRequest.DONE },
status: { value: 200 },
status: { value: HTTP_STATUS_OK },
response: { value: responseText },
});
this.onreadystatechange();

View File

@ -4,7 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import createStore from '~/milestones/stores/';
@ -64,15 +64,15 @@ describe('Milestone combobox component', () => {
projectMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
.mockReturnValue([HTTP_STATUS_OK, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
.mockReturnValue([HTTP_STATUS_OK, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
searchApiCallSpy = jest
.fn()
.mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
.mockReturnValue([HTTP_STATUS_OK, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
mock
.onGet(`/api/v4/projects/${projectId}/milestones`)
@ -248,9 +248,11 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@ -301,7 +303,7 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@ -366,7 +368,7 @@ describe('Milestone combobox component', () => {
createComponent();
projectMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
.mockReturnValue([HTTP_STATUS_OK, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
return waitForRequests();
});
@ -430,7 +432,7 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@ -495,7 +497,11 @@ describe('Milestone combobox component', () => {
createComponent();
groupMilestonesApiCallSpy = jest
.fn()
.mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
.mockReturnValue([
HTTP_STATUS_OK,
[{ title: 'group-v1.0' }],
{ [X_TOTAL_HEADER]: '1' },
]);
return waitForRequests();
});

View File

@ -1,4 +1,5 @@
// Copied to ee/spec/frontend/notes/mock_data.js
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { __ } from '~/locale';
export const notesDataMock = {
@ -655,11 +656,11 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
};
export function getIndividualNoteResponse(config) {
return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
return [HTTP_STATUS_OK, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
}
export function getDiscussionNoteResponse(config) {
return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
return [HTTP_STATUS_OK, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
}
export const notesWithDescriptionChanges = [

View File

@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UpdateUsername from '~/profile/account/components/update_username.vue';
jest.mock('~/flash');
@ -97,7 +97,7 @@ describe('UpdateUsername component', () => {
});
it('executes API call on confirmation button click', async () => {
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
await wrapper.vm.onConfirm();
@ -114,7 +114,7 @@ describe('UpdateUsername component', () => {
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
return [200, { message: 'Username changed' }];
return [HTTP_STATUS_OK, { message: 'Username changed' }];
});
await wrapper.vm.onConfirm();

View File

@ -9,7 +9,11 @@ import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json';
import tags from 'test_fixtures/api/tags/tags.json';
import { trimText } from 'helpers/text_helper';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
@ -69,9 +73,11 @@ describe('Ref selector component', () => {
branchesApiCallSpy = jest
.fn()
.mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
.mockReturnValue([HTTP_STATUS_OK, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
tagsApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
mock
@ -309,8 +315,10 @@ describe('Ref selector component', () => {
describe('when no results are found', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
branchesApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_NOT_FOUND]);
createComponent();
@ -386,7 +394,9 @@ describe('Ref selector component', () => {
describe('when the branches search returns no results', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
branchesApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@ -451,7 +461,9 @@ describe('Ref selector component', () => {
describe('when the tags search returns no results', () => {
beforeEach(() => {
tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
tagsApiCallSpy = jest
.fn()
.mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();

View File

@ -3,7 +3,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
@ -23,7 +23,6 @@ describe('Terraform extension', () => {
let mock;
const endpoint = '/path/to/terraform/report.json';
const successStatusCode = 200;
const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
@ -57,7 +56,7 @@ describe('Terraform extension', () => {
describe('while loading', () => {
const loadingText = 'Loading Terraform reports...';
it('should render loading text', async () => {
mockPollingApi(successStatusCode, plans, {});
mockPollingApi(HTTP_STATUS_OK, plans, {});
createComponent();
expect(wrapper.text()).toContain(loadingText);
@ -85,7 +84,7 @@ describe('Terraform extension', () => {
${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
`('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
beforeEach(async () => {
mockPollingApi(successStatusCode, response, {});
mockPollingApi(HTTP_STATUS_OK, response, {});
return createComponent();
});
@ -102,7 +101,7 @@ describe('Terraform extension', () => {
describe('expanded data', () => {
beforeEach(async () => {
mockPollingApi(successStatusCode, plans, {});
mockPollingApi(HTTP_STATUS_OK, plans, {});
await createComponent();
wrapper.findByTestId('toggle-button').trigger('click');
@ -164,7 +163,7 @@ describe('Terraform extension', () => {
describe('successful poll', () => {
beforeEach(() => {
mockPollingApi(successStatusCode, plans, {});
mockPollingApi(HTTP_STATUS_OK, plans, {});
return createComponent();
});

View File

@ -1,9 +1,10 @@
import { Response } from 'miragejs';
import emojis from 'public/-/emojis/2/emojis.json';
import { EMOJI_VERSION } from '~/emoji';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
export default (server) => {
server.get(`/-/emojis/${EMOJI_VERSION}/emojis.json`, () => {
return new Response(200, {}, emojis);
return new Response(HTTP_STATUS_OK, {}, emojis);
});
};

View File

@ -1,93 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe RetryBackfillTraversalIds, :migration, feature_category: :subgroups do
include ReloadHelpers
let!(:namespaces_table) { table(:namespaces) }
context 'when BackfillNamespaceTraversalIdsRoots jobs are pending' do
before do
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsRoots',
arguments: [1, 4, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
)
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsRoots',
arguments: [5, 9, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
)
end
it 'queues pending jobs' do
migrate!
expect(BackgroundMigrationWorker.jobs.length).to eq(1)
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['BackfillNamespaceTraversalIdsRoots', [1, 4, 100]])
expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
end
end
context 'when BackfillNamespaceTraversalIdsChildren jobs are pending' do
before do
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsChildren',
arguments: [1, 4, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
)
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsRoots',
arguments: [5, 9, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
)
end
it 'queues pending jobs' do
migrate!
expect(BackgroundMigrationWorker.jobs.length).to eq(1)
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['BackfillNamespaceTraversalIdsChildren', [1, 4, 100]])
expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
end
end
context 'when BackfillNamespaceTraversalIdsRoots and BackfillNamespaceTraversalIdsChildren jobs are pending' do
before do
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsRoots',
arguments: [1, 4, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
)
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsChildren',
arguments: [5, 9, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']
)
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsRoots',
arguments: [11, 14, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
)
table(:background_migration_jobs).create!(
class_name: 'BackfillNamespaceTraversalIdsChildren',
arguments: [15, 19, 100],
status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
)
end
it 'queues pending jobs' do
freeze_time do
migrate!
expect(BackgroundMigrationWorker.jobs.length).to eq(2)
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['BackfillNamespaceTraversalIdsRoots', [1, 4, 100]])
expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['BackfillNamespaceTraversalIdsChildren', [5, 9, 100]])
expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(RetryBackfillTraversalIds::DELAY_INTERVAL.from_now.to_f)
end
end
end
end

View File

@ -2,17 +2,17 @@
require 'spec_helper'
RSpec.describe Clusters::Agents::RefreshAuthorizationService do
RSpec.describe Clusters::Agents::RefreshAuthorizationService, feature_category: :kubernetes_management do
describe '#execute' do
let_it_be(:root_ancestor) { create(:group) }
let_it_be(:removed_group) { create(:group, parent: root_ancestor) }
let_it_be(:modified_group) { create(:group, parent: root_ancestor) }
let_it_be(:added_group) { create(:group, parent: root_ancestor) }
let_it_be(:added_group) { create(:group, path: 'group-path-with-UPPERCASE', parent: root_ancestor) }
let_it_be(:removed_project) { create(:project, namespace: root_ancestor) }
let_it_be(:modified_project) { create(:project, namespace: root_ancestor) }
let_it_be(:added_project) { create(:project, namespace: root_ancestor) }
let_it_be(:added_project) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) }
let(:project) { create(:project, namespace: root_ancestor) }
let(:agent) { create(:cluster_agent, project: project) }
@ -22,11 +22,13 @@ RSpec.describe Clusters::Agents::RefreshAuthorizationService do
ci_access: {
groups: [
{ id: added_group.full_path, default_namespace: 'default' },
{ id: modified_group.full_path, default_namespace: 'new-namespace' }
# Uppercase path verifies case-insensitive matching.
{ id: modified_group.full_path.upcase, default_namespace: 'new-namespace' }
],
projects: [
{ id: added_project.full_path, default_namespace: 'default' },
{ id: modified_project.full_path, default_namespace: 'new-namespace' }
# Uppercase path verifies case-insensitive matching.
{ id: modified_project.full_path.upcase, default_namespace: 'new-namespace' }
]
}
}.deep_stringify_keys