Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-20 21:09:09 +00:00
parent f781b0b693
commit 1902e25626
35 changed files with 254 additions and 112 deletions

View File

@ -9,6 +9,7 @@
/scripts/
/tmp/
/vendor/
jest.config.js
jest.config.*.js
karma.config.js
webpack.config.js

View File

@ -354,7 +354,6 @@ RSpec/LeakyConstantDeclaration:
- 'spec/lib/gitlab/ci/config/entry/retry_spec.rb'
- 'spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb'
- 'spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb'
- 'spec/lib/gitlab/config/entry/factory_spec.rb'
- 'spec/lib/gitlab/config/entry/simplifiable_spec.rb'
- 'spec/lib/gitlab/database/migration_helpers_spec.rb'
- 'spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb'
@ -362,11 +361,8 @@ RSpec/LeakyConstantDeclaration:
- 'spec/lib/gitlab/git/diff_collection_spec.rb'
- 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
- 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
- 'spec/lib/gitlab/jira_import/issues_importer_spec.rb'
- 'spec/lib/gitlab/no_cache_headers_spec.rb'
- 'spec/lib/gitlab/path_regex_spec.rb'
- 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
- 'spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb'
- 'spec/lib/marginalia_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb'

View File

@ -51,6 +51,7 @@ document.addEventListener(
ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath,
projectId: this.dataset.projectId,
params: JSON.parse(this.dataset.params),
},
});
},

View File

@ -1,5 +1,5 @@
<script>
import { isEqual } from 'lodash';
import { isEqual, pickBy } from 'lodash';
import { __, sprintf, s__ } from '../../locale';
import createFlash from '../../flash';
import PipelinesService from '../services/pipelines_service';
@ -10,7 +10,7 @@ import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING } from '../constants';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, SUPPORTED_FILTER_PARAMETERS } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@ -86,6 +86,10 @@ export default {
type: String,
required: true,
},
params: {
type: Object,
required: true,
},
},
data() {
return {
@ -220,10 +224,15 @@ export default {
canFilterPipelines() {
return this.glFeatures.filterPipelinesSearch;
},
validatedParams() {
return pickBy(this.params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
},
},
created() {
this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope };
Object.assign(this.requestData, this.validatedParams);
},
methods: {
successCallback(resp) {
@ -306,6 +315,7 @@ export default {
v-if="canFilterPipelines"
:pipelines="state.pipelines"
:project-id="projectId"
:params="validatedParams"
@filterPipelines="filterPipelines"
/>

View File

@ -3,9 +3,7 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import Api from '~/api';
import createFlash from '~/flash';
import { FETCH_AUTHOR_ERROR_MESSAGE, FETCH_BRANCH_ERROR_MESSAGE } from '../constants';
import { map } from 'lodash';
export default {
components: {
@ -20,12 +18,10 @@ export default {
type: String,
required: true,
},
},
data() {
return {
projectUsers: null,
projectBranches: null,
};
params: {
type: Object,
required: true,
},
},
computed: {
tokens() {
@ -37,7 +33,6 @@ export default {
unique: true,
token: PipelineTriggerAuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
triggerAuthors: this.projectUsers,
projectId: this.projectId,
},
{
@ -47,30 +42,16 @@ export default {
unique: true,
token: PipelineBranchNameToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
branches: this.projectBranches,
projectId: this.projectId,
},
];
},
},
created() {
Api.projectUsers(this.projectId)
.then(users => {
this.projectUsers = users;
})
.catch(err => {
createFlash(FETCH_AUTHOR_ERROR_MESSAGE);
throw err;
});
Api.branches(this.projectId)
.then(({ data }) => {
this.projectBranches = data.map(branch => branch.name);
})
.catch(err => {
createFlash(FETCH_BRANCH_ERROR_MESSAGE);
throw err;
});
paramsValue() {
return map(this.params, (val, key) => ({
type: key,
value: { data: val, operator: '=' },
}));
},
},
methods: {
onSubmit(filters) {
@ -85,6 +66,7 @@ export default {
<gl-filtered-search
:placeholder="__('Filter pipelines')"
:available-tokens="tokens"
:value="paramsValue"
@submit="onSubmit"
/>
</div>

View File

@ -23,15 +23,18 @@ export default {
},
data() {
return {
branches: this.config.branches,
branches: null,
loading: true,
};
},
created() {
this.fetchBranches();
},
methods: {
fetchBranchBySearchTerm(searchTerm) {
Api.branches(this.config.projectId, searchTerm)
.then(res => {
this.branches = res.data.map(branch => branch.name);
fetchBranches(searchterm) {
Api.branches(this.config.projectId, searchterm)
.then(({ data }) => {
this.branches = data.map(branch => branch.name);
this.loading = false;
})
.catch(err => {
@ -41,7 +44,7 @@ export default {
});
},
searchBranches: debounce(function debounceSearch({ data }) {
this.fetchBranchBySearchTerm(data);
this.fetchBranches(data);
}, FILTER_PIPELINES_SEARCH_DELAY),
},
};

View File

@ -36,7 +36,7 @@ export default {
},
data() {
return {
users: this.config.triggerAuthors,
users: [],
loading: true,
};
},
@ -50,11 +50,14 @@ export default {
});
},
},
created() {
this.fetchProjectUsers();
},
methods: {
fetchAuthorBySearchTerm(searchTerm) {
fetchProjectUsers(searchTerm) {
Api.projectUsers(this.config.projectId, searchTerm)
.then(res => {
this.users = res;
.then(users => {
this.users = users;
this.loading = false;
})
.catch(err => {
@ -64,7 +67,7 @@ export default {
});
},
searchAuthors: debounce(function debounceSearch({ data }) {
this.fetchAuthorBySearchTerm(data);
this.fetchProjectUsers(data);
}, FILTER_PIPELINES_SEARCH_DELAY),
},
};

View File

@ -5,6 +5,7 @@ export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300;
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref'];
export const TestStatus = {
FAILED: 'failed',

View File

@ -483,11 +483,12 @@ class ProjectsController < Projects::ApplicationController
def export_rate_limit
prefixed_action = "project_#{params[:action]}".to_sym
if rate_limiter.throttled?(prefixed_action, scope: [current_user, prefixed_action, @project])
project_scope = params[:action] == :download_export ? @project : nil
if rate_limiter.throttled?(prefixed_action, scope: [current_user, prefixed_action, project_scope].compact)
rate_limiter.log_request(request, "#{prefixed_action}_request_limit".to_sym, current_user)
flash[:alert] = _('This endpoint has been requested too many times. Try again later.')
redirect_to edit_project_path(@project)
render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
end
end

View File

@ -4,6 +4,7 @@
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
project_id: @project.id,
params: params.to_json,
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),

View File

@ -0,0 +1,5 @@
---
title: Filter pipelines based on url query params
merge_request: 32230
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Improve Add/Remove Issue Labels API
merge_request: 31864
author: Lee Tickett
type: added

View File

@ -0,0 +1,5 @@
---
title: Rate limit project export by user
merge_request: 31719
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue in sidekiq middleware server metric spec
merge_request: 32104
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue importer and cache headers spec
merge_request: 32122
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue in factory spec
merge_request: 32174
author: Rajendra Kadam
type: fixed

View File

@ -768,6 +768,8 @@ PUT /projects/:id/issues/:issue_iid
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. |
| `add_labels` | string | no | Comma-separated label names to add to an issue. |
| `remove_labels`| string | no | Comma-separated label names to remove from an issue. |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` (requires admin or project owner rights). Empty string or null values are not accepted.|
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, for example `2016-03-11` |

View File

@ -88,6 +88,23 @@ or more users or by the `@name` of one or more groups that should
be owners of the file. Groups must be added as [members of the project](members/index.md),
or they will be ignored.
Starting in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/32432), you can now specify
groups or subgroups from the project's group hierarchy as potential code owners.
For example, consider the following hierarchy for a given project:
```text
group >> sub-group >> sub-subgroup >> myproject >> file.md
```
Any of the following groups would be eligible to be specified as code owners:
- `@group`
- `@group/sub-group`
- `@group/sub-group/sub-subgroup`
In addition, any groups that have been invited to the project using the **Settings > Members** tool will also be recognized as eligible code owners.
The order in which the paths are defined is significant: the last
pattern that matches a given path will be used to find the code
owners.

View File

@ -24,6 +24,8 @@ module API
:discussion_locked,
:due_date,
:labels,
:add_labels,
:remove_labels,
:milestone_id,
:state_event,
:title

View File

@ -67,6 +67,8 @@ module API
optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"

View File

@ -45,7 +45,7 @@ module API
end
end
post ':id/export' do
check_rate_limit! :project_export, [current_user, :project_export, user_project]
check_rate_limit! :project_export, [current_user, :project_export]
project_export_params = declared_params(include_missing: false)
after_export_params = project_export_params.delete(:upload) || {}

View File

@ -10,7 +10,7 @@
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
"file-coverage": "scripts/frontend/file_test_coverage.js",
"prejest": "yarn check-dependencies",
"jest": "jest --config jest.config.unit.js",
"jest": "jest --config jest.config.js",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"jest:integration": "jest --config jest.config.integration.js",
"jsdoc": "jsdoc -c config/jsdocs.config.js",

View File

@ -1159,17 +1159,18 @@ describe ProjectsController do
end
shared_examples 'rate limits project export endpoint' do
before do
allow(::Gitlab::ApplicationRateLimiter)
.to receive(:throttled?)
.and_return(true)
end
it 'prevents requesting project export' do
exportable_project = create(:project)
exportable_project.add_maintainer(user)
post action, params: { namespace_id: exportable_project.namespace, id: exportable_project }
expect(response).to have_gitlab_http_status(:found)
post action, params: { namespace_id: project.namespace, id: project }
expect(flash[:alert]).to eq('This endpoint has been requested too many times. Try again later.')
expect(response).to have_gitlab_http_status(:found)
expect(response.body).to eq('This endpoint has been requested too many times. Try again later.')
expect(response).to have_gitlab_http_status(:too_many_requests)
end
end
@ -1226,7 +1227,18 @@ describe ProjectsController do
end
context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do
include_examples 'rate limits project export endpoint'
before do
allow(::Gitlab::ApplicationRateLimiter)
.to receive(:throttled?)
.and_return(true)
end
it 'prevents requesting project export' do
post action, params: { namespace_id: project.namespace, id: project }
expect(response.body).to eq('This endpoint has been requested too many times. Try again later.')
expect(response).to have_gitlab_http_status(:too_many_requests)
end
end
end
end

View File

@ -10,7 +10,7 @@ settings:
- path
import/resolver:
jest:
jestConfigFile: 'jest.config.unit.js'
jestConfigFile: 'jest.config.js'
globals:
getJSONFixture: false
loadFixtures: false

View File

@ -3,13 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_filtered_search.vue';
import {
users,
mockSearch,
pipelineWithStages,
branches,
mockBranchesAfterMap,
} from '../mock_data';
import { users, mockSearch, pipelineWithStages, branches } from '../mock_data';
import { GlFilteredSearch } from '@gitlab/ui';
describe('Pipelines filtered search', () => {
@ -22,11 +16,12 @@ describe('Pipelines filtered search', () => {
.props('availableTokens')
.find(token => token.type === type);
const createComponent = () => {
const createComponent = (params = {}) => {
wrapper = mount(PipelinesFilteredSearch, {
propsData: {
pipelines: [pipelineWithStages],
projectId: '21',
params,
},
attachToDocument: true,
});
@ -60,7 +55,6 @@ describe('Pipelines filtered search', () => {
icon: 'user',
title: 'Trigger author',
unique: true,
triggerAuthors: users,
projectId: '21',
operators: [expect.objectContaining({ value: '=' })],
});
@ -70,28 +64,49 @@ describe('Pipelines filtered search', () => {
icon: 'branch',
title: 'Branch name',
unique: true,
branches: mockBranchesAfterMap,
projectId: '21',
operators: [expect.objectContaining({ value: '=' })],
});
});
it('fetches and sets project users', () => {
expect(Api.projectUsers).toHaveBeenCalled();
expect(wrapper.vm.projectUsers).toEqual(users);
});
it('fetches and sets branches', () => {
expect(Api.branches).toHaveBeenCalled();
expect(wrapper.vm.projectBranches).toEqual(mockBranchesAfterMap);
});
it('emits filterPipelines on submit with correct filter', () => {
findFilteredSearch().vm.$emit('submit', mockSearch);
expect(wrapper.emitted('filterPipelines')).toBeTruthy();
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
});
describe('Url query params', () => {
const params = {
username: 'deja.green',
ref: 'master',
};
beforeEach(() => {
createComponent(params);
});
it('sets default value if url query params', () => {
const expectedValueProp = [
{
type: 'username',
value: {
data: params.username,
operator: '=',
},
},
{
type: 'ref',
value: {
data: params.ref,
operator: '=',
},
},
{ type: 'filtered-search-term', value: { data: '' } },
];
expect(findFilteredSearch().props('value')).toEqual(expectedValueProp);
expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length);
});
});
});

View File

@ -56,6 +56,7 @@ describe('Pipelines', () => {
propsData: {
store: new Store(),
projectId: '21',
params: {},
...props,
},
methods: {

View File

@ -1,7 +1,8 @@
import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineBranchNameToken from '~/pipelines/components/tokens/pipeline_branch_name_token.vue';
import { branches } from '../mock_data';
import { branches, mockBranchesAfterMap } from '../mock_data';
describe('Pipeline Branch Name Token', () => {
let wrapper;
@ -46,6 +47,8 @@ describe('Pipeline Branch Name Token', () => {
};
beforeEach(() => {
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
createComponent();
});
@ -58,6 +61,13 @@ describe('Pipeline Branch Name Token', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
it('fetches and sets project branches', () => {
expect(Api.branches).toHaveBeenCalled();
expect(wrapper.vm.branches).toEqual(mockBranchesAfterMap);
expect(findLoadingIcon().exists()).toBe(false);
});
describe('displays loading icon correctly', () => {
it('shows loading icon', () => {
createComponent({ stubs }, { loading: true });

View File

@ -1,3 +1,4 @@
import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineTriggerAuthorToken from '~/pipelines/components/tokens/pipeline_trigger_author_token.vue';
@ -45,6 +46,8 @@ describe('Pipeline Trigger Author Token', () => {
};
beforeEach(() => {
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
createComponent();
});
@ -57,6 +60,13 @@ describe('Pipeline Trigger Author Token', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
it('fetches and sets project users', () => {
expect(Api.projectUsers).toHaveBeenCalled();
expect(wrapper.vm.users).toEqual(users);
expect(findLoadingIcon().exists()).toBe(false);
});
describe('displays loading icon correctly', () => {
it('shows loading icon', () => {
createComponent({ stubs }, { loading: true });

View File

@ -4,11 +4,14 @@ require 'spec_helper'
describe Gitlab::Config::Entry::Factory do
describe '#create!' do
class Script < Gitlab::Config::Entry::Node
include Gitlab::Config::Entry::Validatable
before do
stub_const('Script', Class.new(Gitlab::Config::Entry::Node))
Script.class_eval do
include Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
validations do
validates :config, array_of_strings: true
end
end
end

View File

@ -39,8 +39,8 @@ describe Gitlab::JiraImport::IssuesImporter do
end
context 'with results returned' do
JiraIssue = Struct.new(:id)
let_it_be(:jira_issues) { [JiraIssue.new(1), JiraIssue.new(2)] }
jira_issue = Struct.new(:id)
let_it_be(:jira_issues) { [jira_issue.new(1), jira_issue.new(2)] }
def mock_issue_serializer(count)
serializer = instance_double(Gitlab::JiraImport::IssueSerializer, execute: { key: 'data' })

View File

@ -3,8 +3,11 @@
require 'spec_helper'
describe Gitlab::NoCacheHeaders do
class NoCacheTester
include Gitlab::NoCacheHeaders
before do
stub_const('NoCacheTester', Class.new)
NoCacheTester.class_eval do
include Gitlab::NoCacheHeaders
end
end
describe "#no_cache_headers" do

View File

@ -164,9 +164,13 @@ describe Gitlab::SidekiqMiddleware::ServerMetrics do
end
context "when workers are not attributed" do
class TestNonAttributedWorker
include Sidekiq::Worker
before do
stub_const('TestNonAttributedWorker', Class.new)
TestNonAttributedWorker.class_eval do
include Sidekiq::Worker
end
end
let(:worker) { TestNonAttributedWorker.new }
let(:labels) { default_labels.merge(urgency: "") }

View File

@ -301,6 +301,35 @@ describe API::Issues do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'adds relevant labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { add_labels: '1, 2' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly(label.title, '1', '2')
end
context 'removes' do
let!(:label2) { create(:label, title: 'a-label', project: project) }
let!(:label_link2) { create(:label_link, label: label2, target: issue) }
it 'removes relevant labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { remove_labels: label2.title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { remove_labels: "#{label.title}, #{label2.title}" }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to be_empty
end
end
it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
params: { title: 'updated title' }

View File

@ -44,19 +44,6 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
it_behaves_like '404 response'
end
shared_examples_for 'when rate limit is exceeded' do
before do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
end
it 'prevents requesting project export' do
request
expect(response).to have_gitlab_http_status(:too_many_requests)
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end
end
describe 'GET /projects/:project_id/export' do
shared_examples_for 'get project export status not found' do
it_behaves_like '404 response' do
@ -247,7 +234,16 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
context 'when rate limit is exceeded' do
let(:request) { get api(download_path, admin) }
include_examples 'when rate limit is exceeded'
before do
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
end
it 'prevents requesting project export' do
request
expect(response).to have_gitlab_http_status(:too_many_requests)
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end
end
end
@ -360,10 +356,17 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
it_behaves_like 'post project export start'
context 'when rate limit is exceeded' do
let(:request) { post api(path, admin) }
context 'when rate limit is exceeded across projects' do
it 'prevents requesting project export' do
post api(path_none, admin)
include_examples 'when rate limit is exceeded'
expect(response).not_to have_gitlab_http_status(:too_many_requests)
post api(path, admin)
expect(response).to have_gitlab_http_status(:too_many_requests)
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end
end
end