diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 18:12:25 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 18:12:25 +0300 |
commit | 068b3a417794ab8506b2e149301b3a60c01df078 (patch) | |
tree | 26ce51b45ae535a6fc47fb04cad8da42ec408a2f /spec | |
parent | 62c78157be8fe8888787162293f13945a5fa5d3e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
18 files changed, 748 insertions, 271 deletions
diff --git a/spec/controllers/concerns/check_rate_limit_spec.rb b/spec/controllers/concerns/check_rate_limit_spec.rb new file mode 100644 index 00000000000..34ececfe639 --- /dev/null +++ b/spec/controllers/concerns/check_rate_limit_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CheckRateLimit do + let(:key) { :some_key } + let(:scope) { [:some, :scope] } + let(:request) { instance_double('Rack::Request') } + let(:user) { build_stubbed(:user) } + + let(:controller_class) do + Class.new do + include CheckRateLimit + + attr_reader :request, :current_user + + def initialize(request, current_user) + @request = request + @current_user = current_user + end + + def redirect_back_or_default(**args) + end + + def render(**args) + end + end + end + + subject { controller_class.new(request, user) } + + before do + allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?) + allow(::Gitlab::ApplicationRateLimiter).to receive(:log_request) + end + + describe '#check_rate_limit!' do + it 'calls ApplicationRateLimiter#throttled? with the right arguments' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false) + expect(subject).not_to receive(:render) + + subject.check_rate_limit!(key, scope: scope) + end + + it 'renders error and logs request if throttled' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(true) + expect(::Gitlab::ApplicationRateLimiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user) + expect(subject).to receive(:render).with({ plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests }) + + subject.check_rate_limit!(key, scope: scope) + end + + it 'redirects back if throttled and redirect_back option is set to true' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(true) + expect(::Gitlab::ApplicationRateLimiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user) + expect(subject).not_to receive(:render) + expect(subject).to receive(:redirect_back_or_default).with(options: { alert: _('This endpoint has been requested too many times. Try again later.') }) + + subject.check_rate_limit!(key, scope: scope, redirect_back: true) + end + + context 'when the bypass header is set' do + before do + allow(Gitlab::Throttle).to receive(:bypass_header).and_return('SOME_HEADER') + end + + it 'skips rate limit if set to "1"' do + allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('1') + + expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + expect(subject).not_to receive(:render) + + subject.check_rate_limit!(key, scope: scope) + end + + it 'does not skip rate limit if set to something else than "1"' do + allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('0') + + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?) + + subject.check_rate_limit!(key, scope: scope) + end + end + end +end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index 0d797b7923c..a9a8e9d19b8 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -3,83 +3,93 @@ require 'spec_helper' RSpec.describe GroupMembersFinder, '#execute' do - let(:group) { create(:group) } - let(:sub_group) { create(:group, parent: group) } - let(:sub_sub_group) { create(:group, parent: sub_group) } - let(:user1) { create(:user) } - let(:user2) { create(:user) } - let(:user3) { create(:user) } - let(:user4) { create(:user) } - let(:user5) { create(:user, :two_factor_via_otp) } + let_it_be(:group) { create(:group) } + let_it_be(:sub_group) { create(:group, parent: group) } + let_it_be(:sub_sub_group) { create(:group, parent: sub_group) } + let_it_be(:public_shared_group) { create(:group, :public) } + let_it_be(:private_shared_group) { create(:group, :private) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:user3) { create(:user) } + let_it_be(:user4) { create(:user) } + let_it_be(:user5) { create(:user, :two_factor_via_otp) } + + let!(:link) do + create(:group_group_link, shared_group: group, shared_with_group: public_shared_group) + create(:group_group_link, shared_group: sub_group, shared_with_group: private_shared_group) + end let(:groups) do { - group: group, - sub_group: sub_group, - sub_sub_group: sub_sub_group + group: group, + sub_group: sub_group, + sub_sub_group: sub_sub_group, + public_shared_group: public_shared_group, + private_shared_group: private_shared_group } end context 'relations' do let!(:members) do { - user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1), - user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1), - user1_group: create(:group_member, :reporter, group: group, user: user1), - user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2), - user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2), - user2_group: create(:group_member, :maintainer, group: group, user: user2), - user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now), - user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now), - user3_group: create(:group_member, :reporter, group: group, user: user3), - user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4), - user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now), - user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now) + user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1), + user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1), + user1_group: create(:group_member, :reporter, group: group, user: user1), + user1_public_shared_group: create(:group_member, :maintainer, group: public_shared_group, user: user1), + user1_private_shared_group: create(:group_member, :maintainer, group: private_shared_group, user: user1), + user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2), + user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2), + user2_group: create(:group_member, :maintainer, group: group, user: user2), + user2_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user2), + user2_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user2), + user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now), + user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now), + user3_group: create(:group_member, :reporter, group: group, user: user3), + user3_public_shared_group: create(:group_member, :reporter, group: public_shared_group, user: user3), + user3_private_shared_group: create(:group_member, :reporter, group: private_shared_group, user: user3), + user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4), + user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now), + user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now), + user4_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user4), + user4_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user4) } end it 'raises an error if a non-supported relation type is used' do expect do described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type]) - end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants.") + end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants, shared_from_groups.") end using RSpec::Parameterized::TableSyntax where(:subject_relations, :subject_group, :expected_members) do - nil | :group | [:user1_group, :user2_group, :user3_group, :user4_group] - [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group] - [:inherited] | :group | [] - [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group] - [:direct, :inherited] | :group | [:user1_group, :user2_group, :user3_group, :user4_group] - [:direct, :descendants] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group] - [:direct, :descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] - nil | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group] - [:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group] - [:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group] - [:direct, :inherited] | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:direct, :descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group] - [:descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_sub_group, :user4_group] - [:direct, :descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] - nil | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group] - [:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:descendants] | :sub_sub_group | [] - [:direct, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:direct, :descendants] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group] - [:descendants, :inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group] - [:direct, :descendants, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] + [] | :group | [] + GroupMembersFinder::DEFAULT_RELATIONS | :group | [:user1_group, :user2_group, :user3_group, :user4_group] + [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group] + [:inherited] | :group | [] + [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group] + [:shared_from_groups] | :group | [:user1_public_shared_group, :user2_public_shared_group, :user3_public_shared_group, :user4_public_shared_group] + [:direct, :inherited, :descendants, :shared_from_groups] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_public_shared_group] + [] | :sub_group | [] + GroupMembersFinder::DEFAULT_RELATIONS | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group] + [:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group] + [:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group] + [:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group] + [:shared_from_groups] | :sub_group | [] + [:direct, :inherited, :descendants, :shared_from_groups] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] + [] | :sub_sub_group | [] + GroupMembersFinder::DEFAULT_RELATIONS | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] + [:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group] + [:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group] + [:descendants] | :sub_sub_group | [] + [:shared_from_groups] | :sub_sub_group | [] + [:direct, :inherited, :descendants, :shared_from_groups] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group] end with_them do it 'returns correct members' do - result = if subject_relations - described_class.new(groups[subject_group]).execute(include_relations: subject_relations) - else - described_class.new(groups[subject_group]).execute - end + result = described_class.new(groups[subject_group]).execute(include_relations: subject_relations) expect(result.to_a).to match_array(expected_members.map { |name| members[name] }) end diff --git a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js index ae280146c22..8e46af5dfd6 100644 --- a/spec/frontend/vue_mr_widget/components/terraform/mock_data.js +++ b/spec/frontend/vue_mr_widget/components/terraform/mock_data.js @@ -1,6 +1,6 @@ export const invalidPlanWithName = { job_name: 'Invalid Plan', - job_path: '/path/to/ci/logs/1', + job_path: '/path/to/ci/logs/3', tf_report_error: 'api_error', }; @@ -20,12 +20,12 @@ export const validPlanWithoutName = { create: 10, update: 20, delete: 30, - job_path: '/path/to/ci/logs/1', + job_path: '/path/to/ci/logs/2', }; export const plans = { invalid_plan_one: invalidPlanWithName, - invalid_plan_two: invalidPlanWithName, + invalid_plan_two: invalidPlanWithoutName, valid_plan_one: validPlanWithName, valid_plan_two: validPlanWithoutName, }; diff --git a/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js new file mode 100644 index 00000000000..f8ea6fc23a2 --- /dev/null +++ b/spec/frontend/vue_mr_widget/extentions/terraform/index_spec.js @@ -0,0 +1,178 @@ +import MockAdapter from 'axios-mock-adapter'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +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'; +import terraformExtension from '~/vue_merge_request_widget/extensions/terraform'; +import { + plans, + validPlanWithName, + validPlanWithoutName, + invalidPlanWithName, + invalidPlanWithoutName, +} from '../../components/terraform/mock_data'; + +describe('Terraform extension', () => { + let wrapper; + let mock; + + const endpoint = '/path/to/terraform/report.json'; + const successStatusCode = 200; + const errorStatusCode = 500; + + const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at); + + registerExtension(terraformExtension); + + const mockPollingApi = (response, body, header) => { + mock.onGet(endpoint).reply(response, body, header); + }; + + const createComponent = () => { + wrapper = mountExtended(extensionsContainer, { + propsData: { + mr: { + terraformReportsPath: endpoint, + }, + }, + }); + return axios.waitForAll(); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('summary', () => { + describe('while loading', () => { + const loadingText = 'Loading Terraform reports...'; + it('should render loading text', async () => { + mockPollingApi(successStatusCode, plans, {}); + createComponent(); + + expect(wrapper.text()).toContain(loadingText); + await waitForPromises(); + expect(wrapper.text()).not.toContain(loadingText); + }); + }); + + describe('when the fetching fails', () => { + beforeEach(() => { + mockPollingApi(errorStatusCode, null, {}); + return createComponent(); + }); + + it('should generate one invalid plan and render correct summary text', () => { + expect(wrapper.text()).toContain('1 Terraform report failed to generate'); + }); + }); + + describe('when the fetching succeeds', () => { + describe.each` + responseType | response | summaryTitle | summarySubtitle + ${'1 invalid report'} | ${{ 0: invalidPlanWithName }} | ${'1 Terraform report failed to generate'} | ${''} + ${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''} + ${'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, {}); + return createComponent(); + }); + + it(`should render correct summary text`, () => { + expect(wrapper.text()).toContain(summaryTitle); + + if (summarySubtitle) { + expect(wrapper.text()).toContain(summarySubtitle); + } + }); + }); + }); + }); + + describe('expanded data', () => { + beforeEach(async () => { + mockPollingApi(successStatusCode, plans, {}); + await createComponent(); + + wrapper.findByTestId('toggle-button').trigger('click'); + }); + + describe.each` + reportType | title | subtitle | logLink | lineNumber + ${'a valid report with name'} | ${`The job ${validPlanWithName.job_name} generated a report.`} | ${`Reported Resource Changes: ${validPlanWithName.create} to add, ${validPlanWithName.update} to change, ${validPlanWithName.delete} to delete`} | ${validPlanWithName.job_path} | ${0} + ${'a valid report without name'} | ${'A Terraform report was generated in your pipelines.'} | ${`Reported Resource Changes: ${validPlanWithoutName.create} to add, ${validPlanWithoutName.update} to change, ${validPlanWithoutName.delete} to delete`} | ${validPlanWithoutName.job_path} | ${1} + ${'an invalid report with name'} | ${`The job ${invalidPlanWithName.job_name} failed to generate a report.`} | ${'Generating the report caused an error.'} | ${invalidPlanWithName.job_path} | ${2} + ${'an invalid report without name'} | ${'A Terraform report failed to generate.'} | ${'Generating the report caused an error.'} | ${invalidPlanWithoutName.job_path} | ${3} + `('renders correct text for $reportType', ({ title, subtitle, logLink, lineNumber }) => { + it('renders correct text', () => { + expect(findListItem(lineNumber).text()).toContain(title); + expect(findListItem(lineNumber).text()).toContain(subtitle); + }); + + it(`${logLink ? 'renders' : "doesn't render"} the log link`, () => { + const logText = 'Full log'; + if (logLink) { + expect( + findListItem(lineNumber) + .find('[data-testid="extension-actions-button"]') + .attributes('href'), + ).toBe(logLink); + } else { + expect(findListItem(lineNumber).text()).not.toContain(logText); + } + }); + }); + }); + + describe('polling', () => { + let pollRequest; + let pollStop; + + beforeEach(() => { + pollRequest = jest.spyOn(Poll.prototype, 'makeRequest'); + pollStop = jest.spyOn(Poll.prototype, 'stop'); + }); + + afterEach(() => { + pollRequest.mockRestore(); + pollStop.mockRestore(); + }); + + describe('successful poll', () => { + beforeEach(() => { + mockPollingApi(successStatusCode, plans, {}); + + return createComponent(); + }); + + it('does not make additional requests after poll is successful', () => { + expect(pollRequest).toHaveBeenCalledTimes(1); + expect(pollStop).toHaveBeenCalledTimes(1); + }); + }); + + describe('polling fails', () => { + beforeEach(() => { + mockPollingApi(errorStatusCode, null, {}); + return createComponent(); + }); + + it('generates one broken plan', () => { + expect(wrapper.text()).toContain('1 Terraform report failed to generate'); + }); + + it('does not make additional requests after poll is unsuccessful', () => { + expect(pollRequest).toHaveBeenCalledTimes(1); + expect(pollStop).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/spec/graphql/types/group_member_relation_enum_spec.rb b/spec/graphql/types/group_member_relation_enum_spec.rb index 315809ef75e..89ee8c574c4 100644 --- a/spec/graphql/types/group_member_relation_enum_spec.rb +++ b/spec/graphql/types/group_member_relation_enum_spec.rb @@ -6,6 +6,6 @@ RSpec.describe Types::GroupMemberRelationEnum do specify { expect(described_class.graphql_name).to eq('GroupMemberRelation') } it 'exposes all the existing group member relation type values' do - expect(described_class.values.keys).to contain_exactly('DIRECT', 'INHERITED', 'DESCENDANTS') + expect(described_class.values.keys).to contain_exactly('DIRECT', 'INHERITED', 'DESCENDANTS', 'SHARED_FROM_GROUPS') end end diff --git a/spec/lib/api/helpers/rate_limiter_spec.rb b/spec/lib/api/helpers/rate_limiter_spec.rb new file mode 100644 index 00000000000..2fed1cf3604 --- /dev/null +++ b/spec/lib/api/helpers/rate_limiter_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Helpers::RateLimiter do + let(:key) { :some_key } + let(:scope) { [:some, :scope] } + let(:request) { instance_double('Rack::Request') } + let(:user) { build_stubbed(:user) } + + let(:api_class) do + Class.new do + include API::Helpers::RateLimiter + + attr_reader :request, :current_user + + def initialize(request, current_user) + @request = request + @current_user = current_user + end + + def render_api_error!(**args) + end + end + end + + subject { api_class.new(request, user) } + + before do + allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?) + allow(::Gitlab::ApplicationRateLimiter).to receive(:log_request) + end + + describe '#check_rate_limit!' do + it 'calls ApplicationRateLimiter#throttled? with the right arguments' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(false) + expect(subject).not_to receive(:render_api_error!) + + subject.check_rate_limit!(key, scope: scope) + end + + it 'renders api error and logs request if throttled' do + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(key, scope: scope).and_return(true) + expect(::Gitlab::ApplicationRateLimiter).to receive(:log_request).with(request, "#{key}_request_limit".to_sym, user) + expect(subject).to receive(:render_api_error!).with({ error: _('This endpoint has been requested too many times. Try again later.') }, 429) + + subject.check_rate_limit!(key, scope: scope) + end + + context 'when the bypass header is set' do + before do + allow(Gitlab::Throttle).to receive(:bypass_header).and_return('SOME_HEADER') + end + + it 'skips rate limit if set to "1"' do + allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('1') + + expect(::Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + expect(subject).not_to receive(:render_api_error!) + + subject.check_rate_limit!(key, scope: scope) + end + + it 'does not skip rate limit if set to something else than "1"' do + allow(request).to receive(:get_header).with(Gitlab::Throttle.bypass_header).and_return('0') + + expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?) + + subject.check_rate_limit!(key, scope: scope) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb index d834b796179..e1e4877cd50 100644 --- a/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/action_cable_sampler_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Metrics::Samplers::ActionCableSampler do let(:action_cable) { instance_double(ActionCable::Server::Base) } - subject { described_class.new(action_cable: action_cable) } + subject { described_class.new(action_cable: action_cable, logger: double) } it_behaves_like 'metrics sampler', 'ACTION_CABLE_SAMPLER' diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 6f1e0480197..a4877208bcf 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do end describe '#sample_gc' do - let!(:sampler) { described_class.new(5) } + let!(:sampler) { described_class.new } let(:gc_reports) { [{ GC_TIME: 0.1 }, { GC_TIME: 0.2 }, { GC_TIME: 0.3 }] } diff --git a/spec/lib/gitlab_edition_spec.rb b/spec/lib/gitlab_edition_spec.rb new file mode 100644 index 00000000000..2f1316819ec --- /dev/null +++ b/spec/lib/gitlab_edition_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabEdition do + before do + # Make sure the ENV is clean + stub_env('FOSS_ONLY', nil) + stub_env('EE_ONLY', nil) + + described_class.instance_variable_set(:@is_ee, nil) + described_class.instance_variable_set(:@is_jh, nil) + end + + after do + described_class.instance_variable_set(:@is_ee, nil) + described_class.instance_variable_set(:@is_jh, nil) + end + + describe '.root' do + it 'returns the root path of the app' do + expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) + end + end + + describe 'extensions' do + context 'when .jh? is true' do + before do + allow(described_class).to receive(:jh?).and_return(true) + end + + it 'returns %w[ee jh]' do + expect(described_class.extensions).to match_array(%w[ee jh]) + end + end + + context 'when .ee? is true' do + before do + allow(described_class).to receive(:jh?).and_return(false) + allow(described_class).to receive(:ee?).and_return(true) + end + + it 'returns %w[ee]' do + expect(described_class.extensions).to match_array(%w[ee]) + end + end + + context 'when neither .jh? and .ee? are true' do + before do + allow(described_class).to receive(:jh?).and_return(false) + allow(described_class).to receive(:ee?).and_return(false) + end + + it 'returns the exyensions according to the current edition' do + expect(described_class.extensions).to be_empty + end + end + end + + describe '.ee? and .jh?' do + def stub_path(*paths, **arguments) + root = Pathname.new('dummy') + pathname = double(:path, **arguments) + + allow(described_class) + .to receive(:root) + .and_return(root) + + allow(root).to receive(:join) + + paths.each do |path| + allow(root) + .to receive(:join) + .with(path) + .and_return(pathname) + end + end + + describe '.ee?' do + context 'for EE' do + before do + stub_path('ee/app/models/license.rb', exist?: true) + end + + context 'when using FOSS_ONLY=1' do + before do + stub_env('FOSS_ONLY', '1') + end + + it 'returns not to be EE' do + expect(described_class).not_to be_ee + end + end + + context 'when using FOSS_ONLY=0' do + before do + stub_env('FOSS_ONLY', '0') + end + + it 'returns to be EE' do + expect(described_class).to be_ee + end + end + + context 'when using default FOSS_ONLY' do + it 'returns to be EE' do + expect(described_class).to be_ee + end + end + end + + context 'for CE' do + before do + stub_path('ee/app/models/license.rb', exist?: false) + end + + it 'returns not to be EE' do + expect(described_class).not_to be_ee + end + end + end + + describe '.jh?' do + context 'for JH' do + before do + stub_path( + 'ee/app/models/license.rb', + 'jh', + exist?: true) + end + + context 'when using default FOSS_ONLY and EE_ONLY' do + it 'returns to be JH' do + expect(described_class).to be_jh + end + end + + context 'when using FOSS_ONLY=1' do + before do + stub_env('FOSS_ONLY', '1') + end + + it 'returns not to be JH' do + expect(described_class).not_to be_jh + end + end + + context 'when using EE_ONLY=1' do + before do + stub_env('EE_ONLY', '1') + end + + it 'returns not to be JH' do + expect(described_class).not_to be_jh + end + end + end + end + end +end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index 869eaf26772..49ba4debe31 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -3,9 +3,19 @@ require 'spec_helper' RSpec.describe Gitlab do - describe '.root' do - it 'returns the root path of the app' do - expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) + %w[root extensions ee? jh?].each do |method_name| + it "delegates #{method_name} to GitlabEdition" do + expect(GitlabEdition).to receive(method_name) + + described_class.public_send(method_name) + end + end + + %w[ee jh].each do |method_name| + it "delegates #{method_name} to GitlabEdition" do + expect(GitlabEdition).to receive(method_name) + + described_class.public_send(method_name) {} end end @@ -248,121 +258,6 @@ RSpec.describe Gitlab do end end - describe 'ee? and jh?' do - before do - # Make sure the ENV is clean - stub_env('FOSS_ONLY', nil) - stub_env('EE_ONLY', nil) - - described_class.instance_variable_set(:@is_ee, nil) - described_class.instance_variable_set(:@is_jh, nil) - end - - after do - described_class.instance_variable_set(:@is_ee, nil) - described_class.instance_variable_set(:@is_jh, nil) - end - - def stub_path(*paths, **arguments) - root = Pathname.new('dummy') - pathname = double(:path, **arguments) - - allow(described_class) - .to receive(:root) - .and_return(root) - - allow(root).to receive(:join) - - paths.each do |path| - allow(root) - .to receive(:join) - .with(path) - .and_return(pathname) - end - end - - describe '.ee?' do - context 'for EE' do - before do - stub_path('ee/app/models/license.rb', exist?: true) - end - - context 'when using FOSS_ONLY=1' do - before do - stub_env('FOSS_ONLY', '1') - end - - it 'returns not to be EE' do - expect(described_class).not_to be_ee - end - end - - context 'when using FOSS_ONLY=0' do - before do - stub_env('FOSS_ONLY', '0') - end - - it 'returns to be EE' do - expect(described_class).to be_ee - end - end - - context 'when using default FOSS_ONLY' do - it 'returns to be EE' do - expect(described_class).to be_ee - end - end - end - - context 'for CE' do - before do - stub_path('ee/app/models/license.rb', exist?: false) - end - - it 'returns not to be EE' do - expect(described_class).not_to be_ee - end - end - end - - describe '.jh?' do - context 'for JH' do - before do - stub_path( - 'ee/app/models/license.rb', - 'jh', - exist?: true) - end - - context 'when using default FOSS_ONLY and EE_ONLY' do - it 'returns to be JH' do - expect(described_class).to be_jh - end - end - - context 'when using FOSS_ONLY=1' do - before do - stub_env('FOSS_ONLY', '1') - end - - it 'returns not to be JH' do - expect(described_class).not_to be_jh - end - end - - context 'when using EE_ONLY=1' do - before do - stub_env('EE_ONLY', '1') - end - - it 'returns not to be JH' do - expect(described_class).not_to be_jh - end - end - end - end - end - describe '.http_proxy_env?' do it 'returns true when lower case https' do stub_env('https_proxy', 'https://my.proxy') diff --git a/spec/lib/sidebars/projects/panel_spec.rb b/spec/lib/sidebars/projects/panel_spec.rb index 2e79ced7039..7e69a2dfe52 100644 --- a/spec/lib/sidebars/projects/panel_spec.rb +++ b/spec/lib/sidebars/projects/panel_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Sidebars::Projects::Panel do - let(:project) { build(:project) } + let_it_be(:project) { create(:project) } + let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } subject { described_class.new(context) } diff --git a/spec/metrics_server/metrics_server_spec.rb b/spec/metrics_server/metrics_server_spec.rb index 4e3c6900875..4f0d5bc5bb2 100644 --- a/spec/metrics_server/metrics_server_spec.rb +++ b/spec/metrics_server/metrics_server_spec.rb @@ -8,18 +8,24 @@ require_relative '../support/helpers/next_instance_of' RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath include NextInstanceOf + let(:metrics_dir) { Dir.mktmpdir } + before do # We do not want this to have knock-on effects on the test process. allow(Gitlab::ProcessManagement).to receive(:modify_signals) end + after do + FileUtils.rm_rf(metrics_dir, secure: true) + end + describe '.spawn' do context 'when in parent process' do it 'forks into a new process and detaches it' do expect(Process).to receive(:fork).and_return(99) expect(Process).to receive(:detach).with(99) - described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics') + described_class.spawn('sidekiq', metrics_dir: metrics_dir) end end @@ -35,13 +41,13 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath expect(server).to receive(:start) end - described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics') + described_class.spawn('sidekiq', metrics_dir: metrics_dir) end it 'resets signal handlers from parent process' do expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT') - described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics', trapped_signals: %i[A B]) + described_class.spawn('sidekiq', metrics_dir: metrics_dir, trapped_signals: %i[A B]) end end end @@ -50,9 +56,9 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) } let(:exporter_double) { double('fake_exporter', start: true) } let(:prometheus_config) { ::Prometheus::Client.configuration } - let(:metrics_dir) { Dir.mktmpdir } let(:settings) { { "fake_exporter" => { "enabled" => true } } } let!(:old_metrics_dir) { prometheus_config.multiprocess_files_dir } + let(:ruby_sampler_double) { double(Gitlab::Metrics::Samplers::RubySampler) } subject(:metrics_server) { described_class.new('fake', metrics_dir, true)} @@ -60,11 +66,13 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class) expect(exporter_class).to receive(:instance).with(settings['fake_exporter'], synchronous: true).and_return(exporter_double) expect(Settings).to receive(:monitoring).and_return(settings) + + allow(Gitlab::Metrics::Samplers::RubySampler).to receive(:initialize_instance).and_return(ruby_sampler_double) + allow(ruby_sampler_double).to receive(:start) end after do Gitlab::Metrics.reset_registry! - FileUtils.rm_rf(metrics_dir, secure: true) prometheus_config.multiprocess_files_dir = old_metrics_dir end @@ -72,6 +80,7 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath metrics_server.start expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir + expect(::Prometheus::Client.configuration.pid_provider.call).to eq 'fake_exporter' end it 'ensures that metrics directory exists in correct mode (0700)' do @@ -105,5 +114,11 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath metrics_server.start end + + it 'starts a RubySampler instance' do + expect(ruby_sampler_double).to receive(:start) + + subject.start + end end end diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index 9163a7ef845..e80fa6e3b70 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -937,18 +937,6 @@ RSpec.describe Integrations::Jira do end end - context 'with jira_use_first_ref_by_oid feature flag disabled' do - before do - stub_feature_flags(jira_use_first_ref_by_oid: false) - end - - it 'creates a comment and remote link on Jira' do - expect(subject).to eq(success_message) - expect(WebMock).to have_requested(:post, comment_url).with(body: comment_body).once - expect(WebMock).to have_requested(:post, remote_link_url).once - end - end - it 'tracks usage' do expect(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 31cb0393d7f..06afb5b9a49 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -56,12 +56,16 @@ RSpec.describe 'getting group members information' do context 'member relations' do let_it_be(:child_group) { create(:group, :public, parent: parent_group) } let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) } + let_it_be(:invited_group) { create(:group, :public) } let_it_be(:child_user) { create(:user) } let_it_be(:grandchild_user) { create(:user) } + let_it_be(:invited_user) { create(:user) } + let_it_be(:group_link) { create(:group_group_link, shared_group: child_group, shared_with_group: invited_group) } before_all do child_group.add_guest(child_user) grandchild_group.add_guest(grandchild_user) + invited_group.add_guest(invited_user) end it 'returns direct members' do @@ -71,6 +75,13 @@ RSpec.describe 'getting group members information' do expect_array_response(child_user) end + it 'returns invited members plus inherited members' do + fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :SHARED_FROM_GROUPS] }) + + expect(graphql_errors).to be_nil + expect_array_response(invited_user, user_1, user_2, child_user) + end + it 'returns direct and inherited members' do fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] }) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b93df2f3bae..0fb0150ecc9 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -498,6 +498,10 @@ RSpec.describe API::Users do describe "GET /users/:id" do let_it_be(:user2, reload: true) { create(:user, username: 'another_user') } + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:users_get_by_id, scope: user).and_return(false) + end + it "returns a user by id" do get api("/users/#{user.id}", user) @@ -593,6 +597,55 @@ RSpec.describe API::Users do expect(json_response).not_to have_key('sign_in_count') end + context 'when the rate limit is not exceeded' do + it 'returns a success status' do + expect(Gitlab::ApplicationRateLimiter) + .to receive(:throttled?).with(:users_get_by_id, scope: user) + .and_return(false) + + get api("/users/#{user.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the rate limit is exceeded' do + context 'when feature flag is enabled' do + it 'returns "too many requests" status' do + expect(Gitlab::ApplicationRateLimiter) + .to receive(:throttled?).with(:users_get_by_id, scope: user) + .and_return(true) + + get api("/users/#{user.id}", user) + + expect(response).to have_gitlab_http_status(:too_many_requests) + end + + it 'still allows admin users' do + expect(Gitlab::ApplicationRateLimiter) + .not_to receive(:throttled?) + + get api("/users/#{user.id}", admin) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(rate_limit_user_by_id_endpoint: false) + end + + it 'does not throttle the request' do + expect(Gitlab::ApplicationRateLimiter).not_to receive(:throttled?) + + get api("/users/#{user.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + context 'when job title is present' do let(:job_title) { 'Fullstack Engineer' } diff --git a/spec/rubocop/code_reuse_helpers_spec.rb b/spec/rubocop/code_reuse_helpers_spec.rb index 3220cff1681..d437ada85ee 100644 --- a/spec/rubocop/code_reuse_helpers_spec.rb +++ b/spec/rubocop/code_reuse_helpers_spec.rb @@ -315,76 +315,11 @@ RSpec.describe RuboCop::CodeReuseHelpers do end end - describe '#ee?' do - before do - stub_env('FOSS_ONLY', nil) - allow(File).to receive(:exist?).with(ee_file_path) { true } - end - - it 'returns true when ee/app/models/license.rb exists' do - expect(cop.ee?).to eq(true) - end - end - - describe '#jh?' do - context 'when jh directory exists and EE_ONLY is not set' do - before do - stub_env('EE_ONLY', nil) - - allow(Dir).to receive(:exist?).with(File.expand_path('../../jh', __dir__)) { true } - end - - context 'when ee/app/models/license.rb exists' do - before do - allow(File).to receive(:exist?).with(ee_file_path) { true } - end - - context 'when FOSS_ONLY is not set' do - before do - stub_env('FOSS_ONLY', nil) - end - - it 'returns true' do - expect(cop.jh?).to eq(true) - end - end - - context 'when FOSS_ONLY is set to 1' do - before do - stub_env('FOSS_ONLY', '1') - end + %w[ee? jh?].each do |method_name| + it "delegates #{method_name} to GitlabEdition" do + expect(GitlabEdition).to receive(method_name) - it 'returns false' do - expect(cop.jh?).to eq(false) - end - end - end - - context 'when ee/app/models/license.rb not exist' do - before do - allow(File).to receive(:exist?).with(ee_file_path) { false } - end - - context 'when FOSS_ONLY is not set' do - before do - stub_env('FOSS_ONLY', nil) - end - - it 'returns true' do - expect(cop.jh?).to eq(false) - end - end - - context 'when FOSS_ONLY is set to 1' do - before do - stub_env('FOSS_ONLY', '1') - end - - it 'returns false' do - expect(cop.jh?).to eq(false) - end - end - end + cop.public_send(method_name) end end end diff --git a/spec/support/shared_examples/metrics/sampler_shared_examples.rb b/spec/support/shared_examples/metrics/sampler_shared_examples.rb index ebf199c3a8d..cec540cd120 100644 --- a/spec/support/shared_examples/metrics/sampler_shared_examples.rb +++ b/spec/support/shared_examples/metrics/sampler_shared_examples.rb @@ -2,26 +2,98 @@ RSpec.shared_examples 'metrics sampler' do |env_prefix| context 'when sampling interval is passed explicitly' do - subject { described_class.new(42) } + subject(:sampler) { described_class.new(interval: 42, logger: double) } - specify { expect(subject.interval).to eq(42) } + specify { expect(sampler.interval).to eq(42) } end context 'when sampling interval is passed through the environment' do - subject { described_class.new } + subject(:sampler) { described_class.new(logger: double) } before do stub_env("#{env_prefix}_INTERVAL_SECONDS", '42') end - specify { expect(subject.interval).to eq(42) } + specify { expect(sampler.interval).to eq(42) } end context 'when no sampling interval is passed anywhere' do - subject { described_class.new } + subject(:sampler) { described_class.new(logger: double) } it 'uses the hardcoded default' do - expect(subject.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS) + expect(sampler.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS) + end + end + + describe '#start' do + include WaitHelpers + + subject(:sampler) { described_class.new(interval: 0.1) } + + it 'calls the sample method on the sampler thread' do + sampling_threads = [] + expect(sampler).to receive(:sample).at_least(:once) { sampling_threads << Thread.current } + + sampler.start + + wait_for('sampler has sampled', max_wait_time: 3) { sampling_threads.any? } + expect(sampling_threads.first.name).to eq(sampler.thread_name) + + sampler.stop + end + + context 'with warmup set to true' do + subject(:sampler) { described_class.new(interval: 0.1, warmup: true) } + + it 'calls the sample method first on the caller thread' do + sampling_threads = [] + current_thread = Thread.current + # Instead of sampling, we're keeping track of which thread the sampling happened on. + # We want the first sample to be on the spec thread, which would mean a blocking sample + # before the actual sampler thread starts. + expect(sampler).to receive(:sample).at_least(:once) { sampling_threads << Thread.current } + + sampler.start + + wait_for('sampler has sampled', max_wait_time: 3) { sampling_threads.size == 2 } + + expect(sampling_threads.first).to be(current_thread) + expect(sampling_threads.last.name).to eq(sampler.thread_name) + + sampler.stop + end + end + end + + describe '#safe_sample' do + let(:logger) { Logger.new(File::NULL) } + + subject(:sampler) { described_class.new(logger: logger) } + + it 'calls #sample once' do + expect(sampler).to receive(:sample) + + sampler.safe_sample + end + + context 'when sampling fails with error' do + before do + expect(sampler).to receive(:sample).and_raise "something failed" + end + + it 'recovers from errors' do + expect { sampler.safe_sample }.not_to raise_error + end + + context 'with logger' do + let(:logger) { double('logger') } + + it 'logs errors' do + expect(logger).to receive(:warn).with(an_instance_of(String)) + + expect { sampler.safe_sample }.not_to raise_error + end + end end end end diff --git a/spec/views/shared/nav/_sidebar.html.haml_spec.rb b/spec/views/shared/nav/_sidebar.html.haml_spec.rb index 2eeebdff7a8..0eb945f5624 100644 --- a/spec/views/shared/nav/_sidebar.html.haml_spec.rb +++ b/spec/views/shared/nav/_sidebar.html.haml_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe 'shared/nav/_sidebar.html.haml' do - let(:project) { build(:project, id: non_existing_record_id) } + let_it_be(:project) { create(:project) } + let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } let(:sidebar) { Sidebars::Projects::Panel.new(context) } |