diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-05 15:11:04 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-05 15:11:04 +0300 |
commit | 164ac94bbd2eadc02ab54322a6fe12ed48ae8041 (patch) | |
tree | 6eec29a4fd554eeb6f4a1b296e37bdf6fb5f6f80 /spec | |
parent | 8934df30a36d16ac9de9aebb079e16f16fda6912 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
46 files changed, 1829 insertions, 766 deletions
diff --git a/spec/controllers/activity_pub/projects/releases_controller_spec.rb b/spec/controllers/activity_pub/projects/releases_controller_spec.rb new file mode 100644 index 00000000000..8719756b260 --- /dev/null +++ b/spec/controllers/activity_pub/projects/releases_controller_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::Projects::ReleasesController, feature_category: :groups_and_projects do + include AccessMatchersForController + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:private_project) { create(:project, :repository, :private) } + let_it_be(:developer) { create(:user) } + let_it_be(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) } + let_it_be(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) } + + before_all do + project.add_developer(developer) + end + + shared_examples 'common access controls' do + it 'renders a 200' do + get(action, params: params) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the project is private' do + let(:project) { private_project } + + context 'when user is not logged in' do + it 'renders a 404' do + get(action, params: params) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is a developer' do + before do + sign_in(developer) + end + + it 'still renders a 404' do + get(action, params: params) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when activity_pub feature flag is disabled' do + before do + stub_feature_flags(activity_pub: false) + end + + it 'renders a 404' do + get(action, params: params) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when activity_pub_project feature flag is disabled' do + before do + stub_feature_flags(activity_pub_project: false) + end + + it 'renders a 404' do + get(action, params: params) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + shared_examples_for 'ActivityPub response' do + it 'returns an application/activity+json content_type' do + expect(response.media_type).to eq 'application/activity+json' + end + + it 'is formated as an ActivityStream document' do + expect(json_response['@context']).to eq 'https://www.w3.org/ns/activitystreams' + end + end + + describe 'GET #index' do + before do + get(action, params: params) + end + + let(:action) { :index } + let(:params) { { namespace_id: project.namespace, project_id: project } } + + it_behaves_like 'common access controls' + it_behaves_like 'ActivityPub response' + + it "returns the project's releases actor profile data" do + expect(json_response['id']).to include project_releases_path(project) + end + end + + describe 'GET #outbox' do + before do + get(action, params: params) + end + + let(:action) { :outbox } + let(:params) { { namespace_id: project.namespace, project_id: project, page: page } } + + context 'with no page parameter' do + let(:page) { nil } + + it_behaves_like 'common access controls' + it_behaves_like 'ActivityPub response' + + it "returns the project's releases collection index" do + expect(json_response['id']).to include outbox_project_releases_path(project) + expect(json_response['totalItems']).to eq 2 + end + end + + context 'with a page parameter' do + let(:page) { 1 } + + it_behaves_like 'common access controls' + it_behaves_like 'ActivityPub response' + + it "returns the project's releases list" do + expect(json_response['id']).to include outbox_project_releases_path(project, page: 1) + + names = json_response['orderedItems'].map { |release| release['object']['name'] } + expect(names).to match_array([release_2.name, release_1.name]) + end + end + end +end diff --git a/spec/factories/self_managed_prometheus_alert_event.rb b/spec/factories/self_managed_prometheus_alert_event.rb deleted file mode 100644 index 3a48aba5f54..00000000000 --- a/spec/factories/self_managed_prometheus_alert_event.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :self_managed_prometheus_alert_event do - project - sequence(:payload_key) { |n| "hash payload key #{n}" } - status { SelfManagedPrometheusAlertEvent.status_value_for(:firing) } - title { 'alert' } - query_expression { 'vector(2)' } - started_at { Time.now } - end -end diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb index b5d4ce9546e..1084891b07f 100644 --- a/spec/factories/usage_data.rb +++ b/spec/factories/usage_data.rb @@ -62,7 +62,6 @@ FactoryBot.define do # Alert Issues create(:alert_management_alert, issue: issues[0], project: projects[0]) create(:alert_management_alert, issue: alert_bot_issues[0], project: projects[0]) - create(:self_managed_prometheus_alert_event, related_issues: [issues[1]], project: projects[0]) # Kubernetes agents create(:cluster_agent, project: projects[0]) diff --git a/spec/finders/organizations/groups_finder_spec.rb b/spec/finders/organizations/groups_finder_spec.rb index 972d80ab036..08c5604149b 100644 --- a/spec/finders/organizations/groups_finder_spec.rb +++ b/spec/finders/organizations/groups_finder_spec.rb @@ -3,12 +3,6 @@ require 'spec_helper' RSpec.describe Organizations::GroupsFinder, feature_category: :cell do - include AdminModeHelper - - let(:current_user) { user } - let(:params) { {} } - let(:finder) { described_class.new(organization: organization, current_user: current_user, params: params) } - let_it_be(:organization_user) { create(:organization_user) } let_it_be(:organization) { organization_user.organization } let_it_be(:user) { organization_user.user } @@ -23,6 +17,10 @@ RSpec.describe Organizations::GroupsFinder, feature_category: :cell do create(:group, :private, name: 'no-access', organization: organization) end + let(:current_user) { user } + let(:params) { {} } + let(:finder) { described_class.new(organization: organization, current_user: current_user, params: params) } + before_all do private_group.add_developer(user) public_group.add_developer(user) @@ -40,7 +38,7 @@ RSpec.describe Organizations::GroupsFinder, feature_category: :cell do end context 'when organization is nil' do - let(:finder) { described_class.new(organization: nil, current_user: current_user, params: params) } + let(:organization) { nil } it { is_expected.to be_empty } end diff --git a/spec/finders/organizations/organization_users_finder_spec.rb b/spec/finders/organizations/organization_users_finder_spec.rb new file mode 100644 index 00000000000..d7fba372e40 --- /dev/null +++ b/spec/finders/organizations/organization_users_finder_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Organizations::OrganizationUsersFinder, feature_category: :cell do + let_it_be(:organization) { create(:organization) } + let_it_be(:organization_user_1) { create(:organization_user, organization: organization) } + let_it_be(:organization_user_2) { create(:organization_user, organization: organization) } + let_it_be(:other_organization_user) { create(:organization_user) } + + let(:current_user) { organization_user_1.user } + let(:finder) { described_class.new(organization: organization, current_user: current_user) } + + subject(:result) { finder.execute.to_a } + + describe '#execute' do + context 'when user is not authorized to read the organization' do + let(:current_user) { create(:user) } + + it { is_expected.to be_empty } + end + + context 'when organization is nil' do + let(:organization) { nil } + + it { is_expected.to be_empty } + end + + context 'when user is authorized to read the organization' do + it 'returns all organization users' do + expect(result).to contain_exactly(organization_user_1, organization_user_2) + end + end + end +end diff --git a/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml index 1c1ad65796c..6c3a0ef5ed6 100644 --- a/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml +++ b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml @@ -1,6 +1,6 @@ --- description: -category: Groups::EmailCampaignsController +category: Projects::Pipelines::EmailCampaignsController action: click label_description: property_description: @@ -13,12 +13,12 @@ identifiers: product_section: product_stage: product_group: -milestone: "13.11" +milestone: '13.11' introduced_by_url: distributions: -- ce -- ee + - ce + - ee tiers: -- free -- premium -- ultimate + - free + - premium + - ultimate diff --git a/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml index 174468028b8..3381c73f23e 100644 --- a/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml +++ b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml @@ -1,6 +1,6 @@ --- description: -category: Groups::EmailCampaignsController +category: Projects::Pipelines::EmailCampaignsController action: click label_description: property_description: @@ -13,10 +13,10 @@ identifiers: product_section: product_stage: product_group: -milestone: "13.11" +milestone: '13.11' introduced_by_url: distributions: -- ee + - ee tiers: -#- premium -- ultimate + #- premium + - ultimate diff --git a/spec/frontend/admin/abuse_report/components/report_actions_spec.js b/spec/frontend/admin/abuse_report/components/report_actions_spec.js index 6dd6d0e55c5..0e20630db14 100644 --- a/spec/frontend/admin/abuse_report/components/report_actions_spec.js +++ b/spec/frontend/admin/abuse_report/components/report_actions_spec.js @@ -191,31 +191,4 @@ describe('ReportActions', () => { ); }); }); - - describe('when moderateUserPath is not present', () => { - it('sends the request to updatePath', async () => { - jest.spyOn(axios, 'put'); - axiosMock.onPut(report.updatePath).replyOnce(HTTP_STATUS_OK, {}); - - const reportWithoutModerateUserPath = { ...report }; - delete reportWithoutModerateUserPath.moderateUserPath; - - createComponent({ report: reportWithoutModerateUserPath }); - - clickActionsButton(); - - await nextTick(); - - selectAction(params.user_action); - selectReason(params.reason); - - await nextTick(); - - submitForm(); - - await waitForPromises(); - - expect(axios.put).toHaveBeenCalledWith(report.updatePath, expect.any(Object)); - }); - }); }); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 450eeefd898..ecd2d7f888d 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -1,11 +1,8 @@ -import * as Sentry from '@sentry/browser'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import * as urlUtils from '~/lib/utils/url_utility'; import { safeUrls, unsafeUrls } from './mock_data'; -jest.mock('@sentry/browser'); - const shas = { valid: [ 'ad9be38573f9ee4c4daec22673478c2dd1d81cd8', @@ -434,11 +431,10 @@ describe('URL utility', () => { it('does not navigate to unsafe urls', () => { // eslint-disable-next-line no-script-url const url = 'javascript:alert(document.domain)'; - urlUtils.visitUrl(url); - expect(Sentry.captureException).toHaveBeenCalledWith( - new RangeError(`Only http and https protocols are allowed: ${url}`), - ); + expect(() => { + urlUtils.visitUrl(url); + }).toThrow(new RangeError(`Only http and https protocols are allowed: ${url}`)); }); it('navigates to a page', () => { diff --git a/spec/graphql/types/organizations/organization_type_spec.rb b/spec/graphql/types/organizations/organization_type_spec.rb index 78c5dea7308..26d7c10a715 100644 --- a/spec/graphql/types/organizations/organization_type_spec.rb +++ b/spec/graphql/types/organizations/organization_type_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Organization'], feature_category: :cell do - let(:expected_fields) { %w[groups id name path] } + let(:expected_fields) { %w[groups id name organization_users path] } specify { expect(described_class.graphql_name).to eq('Organization') } specify { expect(described_class).to require_graphql_authorizations(:read_organization) } diff --git a/spec/graphql/types/organizations/organization_user_type_spec.rb b/spec/graphql/types/organizations/organization_user_type_spec.rb new file mode 100644 index 00000000000..876080b0f15 --- /dev/null +++ b/spec/graphql/types/organizations/organization_user_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['OrganizationUser'], feature_category: :cell do + let(:expected_fields) { %w[badges id user] } + + specify { expect(described_class.graphql_name).to eq('OrganizationUser') } + specify { expect(described_class).to require_graphql_authorizations(:read_organization_user) } + specify { expect(described_class).to have_graphql_fields(*expected_fields) } +end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7108d2c1c9b..9fe820ccae9 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -245,80 +245,74 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do end end - describe '#updated_at_by' do + describe '#issuable_initial_data' do let(:user) { create(:user) } - let(:unedited_issuable) { create(:issue) } - let(:edited_issuable) { create(:issue, last_edited_by: user, created_at: 3.days.ago, updated_at: 1.day.ago, last_edited_at: 2.days.ago) } - let(:edited_updated_at_by) do - { - updatedAt: edited_issuable.last_edited_at.to_time.iso8601, - updatedBy: { - name: user.name, - path: user_path(user) - } - } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?).and_return(true) + stub_commonmark_sourcepos_disabled end - it { expect(helper.updated_at_by(unedited_issuable)).to eq({}) } - it { expect(helper.updated_at_by(edited_issuable)).to eq(edited_updated_at_by) } + context 'when issue' do + it 'returns the correct data for an issue' do + issue = create(:issue, author: user, description: 'issue text') + @project = issue.project + + base_data = { + endpoint: "/#{@project.full_path}/-/issues/#{issue.iid}", + updateEndpoint: "/#{@project.full_path}/-/issues/#{issue.iid}.json", + canUpdate: true, + canDestroy: true, + issuableRef: "##{issue.iid}", + markdownPreviewPath: "/#{@project.full_path}/preview_markdown?target_id=#{issue.iid}&target_type=Issue", + markdownDocsPath: '/help/user/markdown', + lockVersion: issue.lock_version, + state: issue.state, + issuableTemplateNamesPath: template_names_path(@project, issue), + initialTitleHtml: issue.title, + initialTitleText: issue.title, + initialDescriptionHtml: '<p dir="auto">issue text</p>', + initialDescriptionText: 'issue text', + initialTaskCompletionStatus: { completed_count: 0, count: 0 } + } - context 'when updated by a deleted user' do - let(:edited_updated_at_by) do - { - updatedAt: edited_issuable.last_edited_at.to_time.iso8601, - updatedBy: { - name: Users::Internal.ghost.name, - path: user_path(Users::Internal.ghost) - } + issue_only_data = { + canCreateIncident: true, + fullPath: issue.project.full_path, + iid: issue.iid, + issuableId: issue.id, + issueType: 'issue', + isHidden: false, + sentryIssueIdentifier: nil, + zoomMeetingUrl: nil } - end - before do - user.destroy! - end + issue_header_data = { + authorId: issue.author.id, + authorName: issue.author.name, + authorUsername: issue.author.username, + authorWebUrl: url_for(user_path(issue.author)), + createdAt: issue.created_at.to_time.iso8601, + isFirstContribution: issue.first_contribution?, + serviceDeskReplyTo: nil + } - it 'returns "Ghost user" as edited_by' do - expect(helper.updated_at_by(edited_issuable.reload)).to eq(edited_updated_at_by) - end - end - end + work_items_data = { + registerPath: '/users/sign_up?redirect_to_referer=yes', + signInPath: '/users/sign_in?redirect_to_referer=yes' + } - describe '#issuable_initial_data' do - let(:user) { create(:user) } + path_data = { + projectPath: @project.path, + projectId: @project.id, + projectNamespace: @project.namespace.path + } - before do - allow(helper).to receive(:current_user).and_return(user) - allow(helper).to receive(:can?).and_return(true) - stub_commonmark_sourcepos_disabled - end + expected = base_data.merge(issue_only_data, issue_header_data, work_items_data, path_data) - it 'returns the correct data for an issue' do - issue = create(:issue, author: user, description: 'issue text') - @project = issue.project - - expected_data = { - endpoint: "/#{@project.full_path}/-/issues/#{issue.iid}", - updateEndpoint: "/#{@project.full_path}/-/issues/#{issue.iid}.json", - canUpdate: true, - canDestroy: true, - issuableRef: "##{issue.iid}", - markdownPreviewPath: "/#{@project.full_path}/preview_markdown?target_id=#{issue.iid}&target_type=Issue", - markdownDocsPath: '/help/user/markdown', - lockVersion: issue.lock_version, - projectPath: @project.path, - projectId: @project.id, - projectNamespace: @project.namespace.path, - state: issue.state, - initialTitleHtml: issue.title, - initialTitleText: issue.title, - initialDescriptionHtml: '<p dir="auto">issue text</p>', - initialDescriptionText: 'issue text', - initialTaskCompletionStatus: { completed_count: 0, count: 0 }, - issueType: 'issue', - iid: issue.iid.to_s, - isHidden: false - } - expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data)) + expect(helper.issuable_initial_data(issue)).to include(expected) + end end context 'for incident tab' do @@ -350,6 +344,46 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do end end + context 'when edited' do + it 'contains edited metadata' do + edited_issuable = create(:issue, author: user, description: 'issue text', last_edited_by: user, created_at: 3.days.ago, updated_at: 1.day.ago, last_edited_at: 2.days.ago) + @project = edited_issuable.project + + expected = { + updatedAt: edited_issuable.last_edited_at.to_time.iso8601, + updatedBy: { + name: user.name, + path: user_path(user) + } + } + + expect(helper.issuable_initial_data(edited_issuable)).to include(expected) + end + + context 'when updated by a deleted user' do + let(:destroyed_user) { create(:user) } + + before do + destroyed_user.destroy! + end + + it 'returns "Ghost user" for updated by data' do + edited_issuable = create(:issue, author: user, description: 'issue text', last_edited_by: destroyed_user, created_at: 3.days.ago, updated_at: 1.day.ago, last_edited_at: 2.days.ago) + @project = edited_issuable.project + + expected = { + updatedAt: edited_issuable.last_edited_at.to_time.iso8601, + updatedBy: { + name: Users::Internal.ghost.name, + path: user_path(Users::Internal.ghost) + } + } + + expect(helper.issuable_initial_data(edited_issuable.reload)).to include(expected) + end + end + end + describe '#sentryIssueIdentifier' do let(:issue) { create(:issue, author: user) } @@ -569,24 +603,6 @@ RSpec.describe IssuablesHelper, feature_category: :team_planning do end end - describe '#issuable_display_type' do - using RSpec::Parameterized::TableSyntax - - where(:issuable_type, :issuable_display_type) do - :issue | 'issue' - :incident | 'incident' - :merge_request | 'merge request' - end - - with_them do - let(:issuable) { build_stubbed(issuable_type) } - - subject { helper.issuable_display_type(issuable) } - - it { is_expected.to eq(issuable_display_type) } - end - end - describe '#issuable_type_selector_data' do using RSpec::Parameterized::TableSyntax diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index dfd65ebdee8..72fa264698d 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe IssuesHelper do +RSpec.describe IssuesHelper, feature_category: :team_planning do include Features::MergeRequestHelpers let_it_be(:project) { create(:project) } @@ -174,14 +174,13 @@ RSpec.describe IssuesHelper do it 'returns expected result' do expected = { can_create_issue: 'true', + can_create_incident: 'true', can_destroy_issue: 'true', can_reopen_issue: 'true', can_report_spam: 'false', can_update_issue: 'true', - iid: issue.iid, is_issue_author: 'false', issue_path: issue_path(issue), - issue_type: 'issue', new_issue_path: new_project_issue_path(project, { add_related_issue: issue.iid }), project_path: project.full_path, report_abuse_path: add_category_abuse_reports_path, diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index ec63c4af13c..5e28490b2c8 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -1076,4 +1076,47 @@ RSpec.describe API::Helpers, feature_category: :shared do it_behaves_like 'authorized' end end + + describe "attributes_for_keys" do + let(:hash) do + { + existing_key_with_present_value: 'actual value', + existing_key_with_nil_value: nil, + existing_key_with_false_value: false + } + end + + let(:parameters) { ::ActionController::Parameters.new(hash) } + let(:symbol_keys) do + %i[ + existing_key_with_present_value + existing_key_with_nil_value + existing_key_with_false_value + non_existing_key + ] + end + + let(:string_keys) { symbol_keys.map(&:to_s) } + let(:filtered_attrs) do + { + 'existing_key_with_present_value' => 'actual value', + 'existing_key_with_false_value' => false + } + end + + let(:empty_attrs) { {} } + + where(:params, :keys, :attrs_result) do + ref(:hash) | ref(:symbol_keys) | ref(:filtered_attrs) + ref(:hash) | ref(:string_keys) | ref(:empty_attrs) + ref(:parameters) | ref(:symbol_keys) | ref(:filtered_attrs) + ref(:parameters) | ref(:string_keys) | ref(:filtered_attrs) + end + + with_them do + it 'returns the values for given keys' do + expect(helper.attributes_for_keys(keys, params)).to eq(attrs_result) + end + end + end end diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb index 62a52ee5fb9..740cfa767e4 100644 --- a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb +++ b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb @@ -6,7 +6,10 @@ RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout, featur let(:ce_temp_dir) { Dir.mktmpdir } let(:ee_temp_dir) { Dir.mktmpdir } let(:timestamp) { Time.now.utc.strftime('%Y%m%d%H%M%S') } - let(:generator_options) { { 'category' => 'Groups::EmailCampaignsController', 'action' => 'click' } } + + let(:generator_options) do + { 'category' => 'Projects::Pipelines::EmailCampaignsController', 'action' => 'click' } + end before do stub_const("#{described_class}::CE_DIR", ce_temp_dir) diff --git a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb index ab6b1cd6171..cd96af2c056 100644 --- a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb +++ b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb @@ -55,14 +55,6 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do end end - describe '#cta_link' do - subject(:cta_link) { test_class.new(group: group, user: user, series: series).cta_link } - - it 'renders link' do - expect(CGI.unescapeHTML(cta_link)).to include(Gitlab::Routing.url_helpers.group_email_campaigns_url(group, track: :create, series: series)) - end - end - describe '#progress' do subject { test_class.new(group: group, user: user, series: series).progress } diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb index af2da8f20c0..829d788b237 100644 --- a/spec/lib/gitlab/job_waiter_spec.rb +++ b/spec/lib/gitlab/job_waiter_spec.rb @@ -4,13 +4,35 @@ require 'spec_helper' RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do describe '.notify' do - it 'pushes the jid to the named queue' do - key = described_class.new.key + let(:key) { described_class.new.key } + it 'pushes the jid to the named queue', :freeze_time do described_class.notify(key, 123) Gitlab::Redis::SharedState.with do |redis| - expect(redis.ttl(key)).to be > 0 + expect(redis.ttl(key)).to eq(described_class::DEFAULT_TTL) + end + end + + it 'can be passed a custom TTL', :freeze_time do + described_class.notify(key, 123, ttl: 5.minutes) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.ttl(key)).to eq(5.minutes.to_i) + end + end + + context 'when the flag is disabled' do + before do + stub_feature_flags(custom_job_waiter_ttl: false) + end + + it 'ignores the custom TTL', :freeze_time do + described_class.notify(key, 123, ttl: 5.minutes) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.ttl(key)).to eq(described_class::DEFAULT_TTL) + end end end end @@ -23,6 +45,32 @@ RSpec.describe Gitlab::JobWaiter, :redis, feature_category: :shared do end end + describe '.delete_key' do + let(:key) { described_class.generate_key } + + it 'deletes the key' do + described_class.notify(key, '1') + described_class.delete_key(key) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.llen(key)).to eq(0) + end + end + + context 'when key is not a JobWaiter key' do + let(:key) { 'foo' } + + it 'does not delete the key' do + described_class.notify(key, '1') + described_class.delete_key(key) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.llen(key)).to eq(1) + end + end + end + end + describe '#wait' do let(:waiter) { described_class.new(2) } diff --git a/spec/lib/gitlab/manifest_import/metadata_spec.rb b/spec/lib/gitlab/manifest_import/metadata_spec.rb index c55b407088d..011371782fe 100644 --- a/spec/lib/gitlab/manifest_import/metadata_spec.rb +++ b/spec/lib/gitlab/manifest_import/metadata_spec.rb @@ -46,16 +46,6 @@ RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_stat expect(status.repositories).to eq(repositories) end - - it 'reads non-hash-tagged keys if hash-tag keys are missing' do - status = described_class.new(user) - - Gitlab::Redis::SharedState.with do |redis| - redis.set(repositories_key, Gitlab::Json.dump(repositories)) - end - - expect(status.repositories).to eq(repositories) - end end describe '#group_id' do @@ -73,13 +63,5 @@ RSpec.describe Gitlab::ManifestImport::Metadata, :clean_gitlab_redis_shared_stat expect(status.group_id).to eq(3) end - - it 'reads non-hash-tagged keys if hash-tag keys are missing' do - status = described_class.new(user) - - Gitlab::Redis::SharedState.with { |redis| redis.set(group_id_key, 2) } - - expect(status.group_id).to eq(2) - end end end diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb index c44cfdea1cd..4485a30ae66 100644 --- a/spec/lib/gitlab/tracking/standard_context_spec.rb +++ b/spec/lib/gitlab/tracking/standard_context_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Tracking::StandardContext do +RSpec.describe Gitlab::Tracking::StandardContext, feature_category: :service_ping do let(:snowplow_context) { subject.to_context } describe '#to_context' do @@ -76,6 +76,7 @@ RSpec.describe Gitlab::Tracking::StandardContext do it 'holds the correct values', :aggregate_failures do json_data = snowplow_context.to_json.fetch(:data) expect(json_data[:user_id]).to eq(user_id) + expect(json_data[:is_gitlab_team_member]).to eq(nil) expect(json_data[:project_id]).to eq(project_id) expect(json_data[:namespace_id]).to eq(namespace_id) expect(json_data[:plan]).to eq(plan_name) diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 7be10c2e357..143d0484392 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -556,7 +556,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic expect(count_data[:issues_using_zoom_quick_actions]).to eq(3) expect(count_data[:issues_with_embedded_grafana_charts_approx]).to eq(2) expect(count_data[:incident_issues]).to eq(4) - expect(count_data[:issues_created_from_alerts]).to eq(3) + expect(count_data[:issues_created_from_alerts]).to eq(2) expect(count_data[:alert_bot_incident_issues]).to eq(4) expect(count_data[:clusters_enabled]).to eq(6) expect(count_data[:project_clusters_enabled]).to eq(4) diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 7c793399f19..4e217e3a9f7 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -28,7 +28,6 @@ RSpec.describe Issue, feature_category: :team_planning do it { is_expected.to have_many(:resource_milestone_events) } it { is_expected.to have_many(:resource_state_events) } it { is_expected.to have_and_belong_to_many(:prometheus_alert_events) } - it { is_expected.to have_and_belong_to_many(:self_managed_prometheus_alert_events) } it { is_expected.to have_many(:prometheus_alerts) } it { is_expected.to have_many(:issue_email_participants) } it { is_expected.to have_one(:email) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5312ca0ef70..db5b5c914c9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -141,7 +141,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:source_pipelines) } it { is_expected.to have_many(:prometheus_alert_events) } - it { is_expected.to have_many(:self_managed_prometheus_alert_events) } it { is_expected.to have_many(:alert_management_alerts) } it { is_expected.to have_many(:alert_management_http_integrations) } it { is_expected.to have_many(:jira_imports) } diff --git a/spec/policies/organizations/organization_policy_spec.rb b/spec/policies/organizations/organization_policy_spec.rb index e51362227c9..3fcfa63b1b2 100644 --- a/spec/policies/organizations/organization_policy_spec.rb +++ b/spec/policies/organizations/organization_policy_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do let_it_be(:organization) { create(:organization) } + let_it_be(:current_user) { create :user } subject(:policy) { described_class.new(current_user, organization) } @@ -19,6 +20,7 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed(:admin_organization) } it { is_expected.to be_allowed(:read_organization) } + it { is_expected.to be_allowed(:read_organization_user) } end context 'when admin mode is disabled' do @@ -27,13 +29,19 @@ RSpec.describe Organizations::OrganizationPolicy, feature_category: :cell do end end - context 'when the user is an organization user' do - let_it_be(:current_user) { create :user } - + context 'when the user is part of the organization' do before do create :organization_user, organization: organization, user: current_user end + it { is_expected.to be_allowed(:read_organization_user) } + it { is_expected.to be_allowed(:read_organization) } + end + + context 'when the user is not part of the organization' do + it { is_expected.to be_disallowed(:read_organization_user) } + # All organizations are currently public, and hence they are allowed to be read + # even if the user is not a part of the organization. it { is_expected.to be_allowed(:read_organization) } end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 2f0e64cd4da..b7377a45dc9 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::CommitStatuses, feature_category: :continuous_integration do +RSpec.describe API::CommitStatuses, :clean_gitlab_redis_cache, feature_category: :continuous_integration do let_it_be(:project) { create(:project, :repository) } let_it_be(:commit) { project.repository.commit } let_it_be(:guest) { create_user(:guest) } @@ -120,6 +120,7 @@ RSpec.describe API::CommitStatuses, feature_category: :continuous_integration do it "does not return project commits" do expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') end end @@ -134,454 +135,471 @@ RSpec.describe API::CommitStatuses, feature_category: :continuous_integration do end end - describe 'POST /projects/:id/statuses/:sha' do - let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" } + [true, false].each do |flag| + context "when feature flag ci_commit_statuses_api_exclusive_lock is set to #{flag}" do + before do + stub_feature_flags(ci_commit_statuses_api_exclusive_lock: flag) + end - context 'developer user' do - context 'uses only required parameters' do - %w[pending running success failed canceled].each do |status| - context "for #{status}" do - context 'when pipeline for sha does not exists' do - it 'creates commit status and sets pipeline iid' do - post api(post_url, developer), params: { state: status } + describe 'POST /projects/:id/statuses/:sha' do + let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" } + + context 'developer user' do + context 'uses only required parameters' do + valid_statues = %w[pending running success failed canceled] + valid_statues.each do |status| + context "for #{status}" do + context 'when pipeline for sha does not exists' do + it 'creates commit status and sets pipeline iid' do + post api(post_url, developer), params: { state: status } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq(status) + expect(json_response['name']).to eq('default') + expect(json_response['ref']).not_to be_empty + expect(json_response['target_url']).to be_nil + expect(json_response['description']).to be_nil + expect(json_response['pipeline_id']).not_to be_nil + + if status == 'failed' + expect(CommitStatus.find(json_response['id'])).to be_api_failure + end + + expect(::Ci::Pipeline.last.iid).not_to be_nil + end + end + end + end - expect(response).to have_gitlab_http_status(:created) - expect(json_response['sha']).to eq(commit.id) - expect(json_response['status']).to eq(status) - expect(json_response['name']).to eq('default') - expect(json_response['ref']).not_to be_empty - expect(json_response['target_url']).to be_nil - expect(json_response['description']).to be_nil - expect(json_response['pipeline_id']).not_to be_nil - - if status == 'failed' - expect(CommitStatus.find(json_response['id'])).to be_api_failure + context 'when pipeline already exists for the specified sha' do + let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') } + let(:params) { { state: 'pending' } } + + shared_examples_for 'creates a commit status for the existing pipeline with an external stage' do + it do + expect do + post api(post_url, developer), params: params + end.not_to change { Ci::Pipeline.count } + + job = pipeline.statuses.find_by_name(json_response['name']) + + expect(response).to have_gitlab_http_status(:created) + expect(job.ci_stage.name).to eq('external') + expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + expect(job.ci_stage.pipeline).to eq(pipeline) + expect(job.status).to eq('pending') + expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) end + end - expect(::Ci::Pipeline.last.iid).not_to be_nil + shared_examples_for 'updates the commit status with an external stage' do + before do + post api(post_url, developer), params: { state: 'pending' } + end + + it 'updates the commit status with the external stage' do + post api(post_url, developer), params: { state: 'running' } + job = pipeline.statuses.find_by_name(json_response['name']) + + expect(job.ci_stage.name).to eq('external') + expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + expect(job.ci_stage.pipeline).to eq(pipeline) + expect(job.status).to eq('running') + expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + end end - end - end - end - context 'when pipeline already exists for the specified sha' do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') } - let(:params) { { state: 'pending' } } + context 'with pipeline for merge request' do + let!(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) } + let!(:pipeline) { merge_request.all_pipelines.last } + let(:sha) { pipeline.sha } - shared_examples_for 'creates a commit status for the existing pipeline with an external stage' do - it do - expect do - post api(post_url, developer), params: params - end.not_to change { Ci::Pipeline.count } + it_behaves_like 'creates a commit status for the existing pipeline with an external stage' + end - job = pipeline.statuses.find_by_name(json_response['name']) + context 'when an external stage does not exist' do + context 'when the commit status does not exist' do + it_behaves_like 'creates a commit status for the existing pipeline with an external stage' + end - expect(response).to have_gitlab_http_status(:created) - expect(job.ci_stage.name).to eq('external') - expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) - expect(job.ci_stage.pipeline).to eq(pipeline) - expect(job.status).to eq('pending') - expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + context 'when the commit status exists' do + it_behaves_like 'updates the commit status with an external stage' + end + end + + context 'when an external stage already exists' do + let(:stage) { create(:ci_stage, name: 'external', pipeline: pipeline, position: 1_000_000) } + + context 'when the commit status exists' do + it_behaves_like 'updates the commit status with an external stage' + end + + context 'when the commit status does not exist' do + it_behaves_like 'creates a commit status for the existing pipeline with an external stage' + end + end + end + + context 'when the pipeline does not exist' do + it 'creates a commit status and a stage' do + expect do + post api(post_url, developer), params: { state: 'pending' } + end.to change { Ci::Pipeline.count }.by(1) + job = Ci::Pipeline.last.statuses.find_by_name(json_response['name']) + + expect(job.ci_stage.name).to eq('external') + expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + expect(job.status).to eq('pending') + expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + end end end - shared_examples_for 'updates the commit status with an external stage' do + context 'when status transitions from pending' do before do post api(post_url, developer), params: { state: 'pending' } end - it 'updates the commit status with the external stage' do - post api(post_url, developer), params: { state: 'running' } - job = pipeline.statuses.find_by_name(json_response['name']) + valid_statues = %w[running success failed canceled] + valid_statues.each do |status| + it "to #{status}" do + expect { post api(post_url, developer), params: { state: status } }.not_to change { CommitStatus.count } - expect(job.ci_stage.name).to eq('external') - expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) - expect(job.ci_stage.pipeline).to eq(pipeline) - expect(job.status).to eq('running') - expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['status']).to eq(status) + end end end - context 'with pipeline for merge request' do - let!(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project) } - let!(:pipeline) { merge_request.all_pipelines.last } - let(:sha) { pipeline.sha } + context 'with all optional parameters' do + context 'when creating a commit status' do + subject do + post api(post_url, developer), params: { + state: 'success', + context: 'coverage', + ref: 'master', + description: 'test', + coverage: 80.0, + target_url: 'http://gitlab.com/status' + } + end - it_behaves_like 'creates a commit status for the existing pipeline with an external stage' - end + it 'creates commit status' do + subject - context 'when an external stage does not exist' do - context 'when the commit status does not exist' do - it_behaves_like 'creates a commit status for the existing pipeline with an external stage' - end + expect(response).to have_gitlab_http_status(:created) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('master') + expect(json_response['coverage']).to eq(80.0) + expect(json_response['description']).to eq('test') + expect(json_response['target_url']).to eq('http://gitlab.com/status') + end - context 'when the commit status exists' do - it_behaves_like 'updates the commit status with an external stage' - end - end + context 'when merge request exists for given branch' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'develop') } - context 'when an external stage already exists' do - let(:stage) { create(:ci_stage, name: 'external', pipeline: pipeline, position: 1_000_000) } + it 'sets head pipeline' do + subject - context 'when the commit status exists' do - it_behaves_like 'updates the commit status with an external stage' + expect(response).to have_gitlab_http_status(:created) + expect(merge_request.reload.head_pipeline).not_to be_nil + end + end end - context 'when the commit status does not exist' do - it_behaves_like 'creates a commit status for the existing pipeline with an external stage' - end - end - end + context 'when updating a commit status' do + let(:parameters) do + { + state: 'success', + name: 'coverage', + ref: 'master' + } + end - context 'when the pipeline does not exist' do - it 'creates a commit status and a stage' do - expect do - post api(post_url, developer), params: { state: 'pending' } - end.to change { Ci::Pipeline.count }.by(1) - job = Ci::Pipeline.last.statuses.find_by_name(json_response['name']) + let(:updatable_optional_attributes) do + { + description: 'new description', + coverage: 90.0 + } + end - expect(job.ci_stage.name).to eq('external') - expect(job.ci_stage.position).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) - expect(job.status).to eq('pending') - expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) - end - end - end + # creating the initial commit status + before do + post api(post_url, developer), params: { + state: 'running', + context: 'coverage', + ref: 'master', + description: 'coverage test', + coverage: 10.0, + target_url: 'http://gitlab.com/status' + } + end - context 'transitions status from pending' do - before do - post api(post_url, developer), params: { state: 'pending' } - end + subject(:send_request) do + post api(post_url, developer), params: { + **parameters, + **updatable_optional_attributes + } + end - %w[running success failed canceled].each do |status| - it "to #{status}" do - expect { post api(post_url, developer), params: { state: status } }.not_to change { CommitStatus.count } + it 'updates a commit status' do + send_request - expect(response).to have_gitlab_http_status(:created) - expect(json_response['status']).to eq(status) - end - end - end + expect(response).to have_gitlab_http_status(:created) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + expect(json_response['name']).to eq('coverage') + expect(json_response['ref']).to eq('master') + expect(json_response['coverage']).to eq(90.0) + expect(json_response['description']).to eq('new description') + expect(json_response['target_url']).to eq('http://gitlab.com/status') + end - context 'with all optional parameters' do - context 'when creating a commit status' do - subject do - post api(post_url, developer), params: { - state: 'success', - context: 'coverage', - ref: 'master', - description: 'test', - coverage: 80.0, - target_url: 'http://gitlab.com/status' - } - end + it 'does not create a new commit status' do + expect { send_request }.not_to change { CommitStatus.count } + end - it 'creates commit status' do - subject - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['sha']).to eq(commit.id) - expect(json_response['status']).to eq('success') - expect(json_response['name']).to eq('coverage') - expect(json_response['ref']).to eq('master') - expect(json_response['coverage']).to eq(80.0) - expect(json_response['description']).to eq('test') - expect(json_response['target_url']).to eq('http://gitlab.com/status') - end + context 'when the `state` parameter is sent the same' do + let(:parameters) do + { + state: 'running', + name: 'coverage', + ref: 'master' + } + end - context 'when merge request exists for given branch' do - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'develop') } + it 'does not update the commit status' do + send_request - it 'sets head pipeline' do - subject + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq("Cannot transition status via :run from :running (Reason(s): Status cannot transition via \"run\")") - expect(response).to have_gitlab_http_status(:created) - expect(merge_request.reload.head_pipeline).not_to be_nil + commit_status = project.commit_statuses.find_by!(name: 'coverage') + + expect(commit_status.description).to eq('coverage test') + expect(commit_status.coverage).to eq(10.0) + end + end end - end - end - context 'when updating a commit status' do - let(:parameters) do - { - state: 'success', - name: 'coverage', - ref: 'master' - } - end + context 'when a pipeline id is specified' do + let!(:first_pipeline) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end - let(:updatable_optional_attributes) do - { - description: 'new description', - coverage: 90.0 - } - end + let!(:other_pipeline) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end - # creating the initial commit status - before do - post api(post_url, developer), params: { - state: 'running', - context: 'coverage', - ref: 'master', - description: 'coverage test', - coverage: 10.0, - target_url: 'http://gitlab.com/status' - } - end + subject do + post api(post_url, developer), params: { + pipeline_id: other_pipeline.id, + state: 'success', + ref: 'master' + } + end - subject(:send_request) do - post api(post_url, developer), params: { - **parameters, - **updatable_optional_attributes - } - end + it 'update the correct pipeline', :sidekiq_might_not_need_inline do + subject - it 'updates a commit status' do - send_request - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['sha']).to eq(commit.id) - expect(json_response['status']).to eq('success') - expect(json_response['name']).to eq('coverage') - expect(json_response['ref']).to eq('master') - expect(json_response['coverage']).to eq(90.0) - expect(json_response['description']).to eq('new description') - expect(json_response['target_url']).to eq('http://gitlab.com/status') + expect(first_pipeline.reload.status).to eq('created') + expect(other_pipeline.reload.status).to eq('success') + end + end end - it 'does not create a new commit status' do - expect { send_request }.not_to change { CommitStatus.count } - end + context 'when retrying a commit status' do + subject(:post_request) do + post api(post_url, developer), + params: { state: 'failed', name: 'test', ref: 'master' } - context 'when the `state` parameter is sent the same' do - let(:parameters) do - { - state: 'running', - name: 'coverage', - ref: 'master' - } + post api(post_url, developer), + params: { state: 'success', name: 'test', ref: 'master' } end - it 'does not update the commit status' do - send_request + it 'correctly posts a new commit status' do + post_request - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + end - commit_status = project.commit_statuses.find_by!(name: 'coverage') + it 'retries the commit status', :sidekiq_might_not_need_inline do + post_request - expect(commit_status.description).to eq('coverage test') - expect(commit_status.coverage).to eq(10.0) + expect(CommitStatus.count).to eq 2 + expect(CommitStatus.first).to be_retried + expect(CommitStatus.last.pipeline).to be_success end end - end - context 'when a pipeline id is specified' do - let!(:first_pipeline) do - project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| - p.ensure_project_iid! # Necessary to avoid cross-database modification error - p.save! + context 'when status is invalid' do + before do + post api(post_url, developer), params: { state: 'invalid' } end - end - let!(:other_pipeline) do - project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| - p.ensure_project_iid! # Necessary to avoid cross-database modification error - p.save! + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(nil) end end - subject do - post api(post_url, developer), params: { - pipeline_id: other_pipeline.id, - state: 'success', - ref: 'master' - } - end - - it 'update the correct pipeline', :sidekiq_might_not_need_inline do - subject + context 'when request without a state made' do + before do + post api(post_url, developer) + end - expect(first_pipeline.reload.status).to eq('created') - expect(other_pipeline.reload.status).to eq('success') + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(nil) + end end - end - end - - context 'when retrying a commit status' do - subject(:post_request) do - post api(post_url, developer), - params: { state: 'failed', name: 'test', ref: 'master' } - - post api(post_url, developer), - params: { state: 'success', name: 'test', ref: 'master' } - end - - it 'correctly posts a new commit status' do - post_request - expect(response).to have_gitlab_http_status(:created) - expect(json_response['sha']).to eq(commit.id) - expect(json_response['status']).to eq('success') - end - - it 'retries the commit status', :sidekiq_might_not_need_inline do - post_request - - expect(CommitStatus.count).to eq 2 - expect(CommitStatus.first).to be_retried - expect(CommitStatus.last.pipeline).to be_success - end - end + context 'when updating a protected ref' do + before do + create(:protected_branch, project: project, name: 'master') + post api(post_url, user), params: { state: 'running', ref: 'master' } + end - context 'when status is invalid' do - before do - post api(post_url, developer), params: { state: 'invalid' } - end + context 'with user as developer' do + let(:user) { developer } - it 'does not create commit status' do - expect(response).to have_gitlab_http_status(:bad_request) - end - end + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') + end + end - context 'when request without a state made' do - before do - post api(post_url, developer) - end + context 'with user as maintainer' do + let(:user) { create_user(:maintainer) } - it 'does not create commit status' do - expect(response).to have_gitlab_http_status(:bad_request) - end - end + it 'creates commit status' do + expect(response).to have_gitlab_http_status(:created) + end + end + end - context 'when updating a protected ref' do - before do - create(:protected_branch, project: project, name: 'master') - post api(post_url, user), params: { state: 'running', ref: 'master' } - end + context 'when commit SHA is invalid' do + let(:sha) { 'invalid_sha' } - context 'with user as developer' do - let(:user) { developer } + before do + post api(post_url, developer), params: { state: 'running' } + end - it 'does not create commit status' do - expect(response).to have_gitlab_http_status(:forbidden) + it 'returns not found error' do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Commit Not Found') + end end - end - context 'with user as maintainer' do - let(:user) { create_user(:maintainer) } + context 'when target URL is an invalid address' do + before do + post api(post_url, developer), params: { + state: 'pending', + target_url: 'invalid url' + } + end - it 'creates commit status' do - expect(response).to have_gitlab_http_status(:created) + it 'responds with bad request status and validation errors' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['target_url']) + .to include 'is blocked: Only allowed schemes are http, https' + end end - end - end - context 'when commit SHA is invalid' do - let(:sha) { 'invalid_sha' } + context 'when target URL is an unsupported scheme' do + before do + post api(post_url, developer), params: { + state: 'pending', + target_url: 'git://example.com' + } + end - before do - post api(post_url, developer), params: { state: 'running' } - end + it 'responds with bad request status and validation errors' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['target_url']) + .to include 'is blocked: Only allowed schemes are http, https' + end + end - it 'returns not found error' do - expect(response).to have_gitlab_http_status(:not_found) - end - end + context 'when trying to update a status of a different type' do + let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') } + let!(:ci_build) { create(:ci_build, pipeline: pipeline, name: 'test-job') } + let(:params) { { state: 'pending', name: 'test-job' } } - context 'when target URL is an invalid address' do - before do - post api(post_url, developer), params: { - state: 'pending', - target_url: 'invalid url' - } - end + before do + post api(post_url, developer), params: params + end - it 'responds with bad request status and validation errors' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['target_url']) - .to include 'is blocked: Only allowed schemes are http, https' - end - end + it 'responds with bad request status and validation errors' do + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['name']) + .to include 'has already been taken' + end + end - context 'when target URL is an unsupported scheme' do - before do - post api(post_url, developer), params: { - state: 'pending', - target_url: 'git://example.com' - } - end + context 'with partitions', :ci_partitionable do + let(:current_partition_id) { ci_testing_partition_id } - it 'responds with bad request status and validation errors' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['target_url']) - .to include 'is blocked: Only allowed schemes are http, https' - end - end + before do + allow(Ci::Pipeline) + .to receive(:current_partition_value) { current_partition_id } + end - context 'when trying to update a status of a different type' do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') } - let!(:ci_build) { create(:ci_build, pipeline: pipeline, name: 'test-job') } - let(:params) { { state: 'pending', name: 'test-job' } } + it 'creates records in the current partition' do + expect { post api(post_url, developer), params: { state: 'running' } } + .to change(CommitStatus, :count).by(1) + .and change(Ci::Pipeline, :count).by(1) - before do - post api(post_url, developer), params: params - end + status = CommitStatus.find(json_response['id']) - it 'responds with bad request status and validation errors' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['name']) - .to include 'has already been taken' + expect(status.partition_id).to eq(current_partition_id) + expect(status.pipeline.partition_id).to eq(current_partition_id) + end + end end - end - context 'with partitions', :ci_partitionable do - let(:current_partition_id) { ci_testing_partition_id } + context 'reporter user' do + before do + post api(post_url, reporter), params: { state: 'running' } + end - before do - allow(Ci::Pipeline) - .to receive(:current_partition_value) { current_partition_id } + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') + end end - it 'creates records in the current partition' do - expect { post api(post_url, developer), params: { state: 'running' } } - .to change(CommitStatus, :count).by(1) - .and change(Ci::Pipeline, :count).by(1) - - status = CommitStatus.find(json_response['id']) + context 'guest user' do + before do + post api(post_url, guest), params: { state: 'running' } + end - expect(status.partition_id).to eq(current_partition_id) - expect(status.pipeline.partition_id).to eq(current_partition_id) + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') + end end - end - end - - context 'reporter user' do - before do - post api(post_url, reporter), params: { state: 'running' } - end - - it 'does not create commit status' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'guest user' do - before do - post api(post_url, guest), params: { state: 'running' } - end - it 'does not create commit status' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'unauthorized user' do - before do - post api(post_url) - end + context 'unauthorized user' do + before do + post api(post_url) + end - it 'does not create commit status' do - expect(response).to have_gitlab_http_status(:unauthorized) + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end end end diff --git a/spec/requests/api/graphql/organizations/organization_query_spec.rb b/spec/requests/api/graphql/organizations/organization_query_spec.rb index 2d09a535279..d02158382eb 100644 --- a/spec/requests/api/graphql/organizations/organization_query_spec.rb +++ b/spec/requests/api/graphql/organizations/organization_query_spec.rb @@ -7,16 +7,14 @@ RSpec.describe 'getting organization information', feature_category: :cell do let(:query) { graphql_query_for(:organization, { id: organization.to_global_id }, organization_fields) } let(:current_user) { user } - let(:groups) { graphql_data_at(:organization, :groups, :edges, :node) } + let(:groups) { graphql_data_at(:organization, :groups, :nodes) } let(:organization_fields) do <<~FIELDS id path groups { - edges { - node { - id - } + nodes { + id } } FIELDS @@ -52,7 +50,7 @@ RSpec.describe 'getting organization information', feature_category: :cell do it 'returns the organization as all organizations are public' do request_organization - expect(graphql_data['organization']['id']).to eq(organization.to_global_id.to_s) + expect(graphql_data_at(:organization, :id)).to eq(organization.to_global_id.to_s) end end @@ -71,8 +69,51 @@ RSpec.describe 'getting organization information', feature_category: :cell do it 'returns no groups' do request_organization - expect(graphql_data['organization']).not_to be_nil - expect(graphql_data['organization']['groups']['edges']).to be_empty + expect(graphql_data_at(:organization)).not_to be_nil + expect(graphql_data_at(:organization, :groups, :nodes)).to be_empty + end + end + + context 'when requesting organization user' do + let(:organization_fields) do + <<~FIELDS + organizationUsers { + nodes { + badges + id + user { + id + } + } + } + FIELDS + end + + it 'returns correct organization user fields' do + request_organization + + organization_user_node = graphql_data_at(:organization, :organizationUsers, :nodes).first + expected_attributes = { + "badges" => ["It's you!"], + "id" => organization_user.to_global_id.to_s, + "user" => { "id" => user.to_global_id.to_s } + } + expect(organization_user_node).to match(expected_attributes) + end + + it 'avoids N+1 queries for all the fields' do + base_query_count = ActiveRecord::QueryRecorder.new { run_query } + + organization_user_2 = create(:organization_user, organization: organization) + other_group.add_developer(organization_user_2.user) + + expect { run_query }.not_to exceed_query_limit(base_query_count) + end + + private + + def run_query + run_with_clean_state(query, context: { current_user: current_user }) end end @@ -83,11 +124,9 @@ RSpec.describe 'getting organization information', feature_category: :cell do id path groups(search: "#{search}") { - edges { - node { - id - name - } + nodes { + id + name } } FIELDS @@ -106,12 +145,12 @@ RSpec.describe 'getting organization information', feature_category: :cell do let(:authorized_groups) { [public_group, private_group, other_group] } where(:field, :direction, :sorted_groups) do - 'id' | 'asc' | lazy { authorized_groups.sort_by(&:id) } - 'id' | 'desc' | lazy { authorized_groups.sort_by(&:id).reverse } - 'name' | 'asc' | lazy { authorized_groups.sort_by(&:name) } - 'name' | 'desc' | lazy { authorized_groups.sort_by(&:name).reverse } - 'path' | 'asc' | lazy { authorized_groups.sort_by(&:path) } - 'path' | 'desc' | lazy { authorized_groups.sort_by(&:path).reverse } + 'id' | 'asc' | lazy { authorized_groups.sort_by(&:id) } + 'id' | 'desc' | lazy { authorized_groups.sort_by(&:id).reverse } + 'name' | 'asc' | lazy { authorized_groups.sort_by(&:name) } + 'name' | 'desc' | lazy { authorized_groups.sort_by(&:name).reverse } + 'path' | 'asc' | lazy { authorized_groups.sort_by(&:path) } + 'path' | 'desc' | lazy { authorized_groups.sort_by(&:path).reverse } end with_them do @@ -121,10 +160,8 @@ RSpec.describe 'getting organization information', feature_category: :cell do id path groups(sort: #{sort}) { - edges { - node { - id - } + nodes { + id } } FIELDS diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 8175d8e57b3..1e8397773be 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -496,73 +496,125 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_manageme Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: user_access_config).execute end - it 'returns 400 when cookie is invalid' do - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: '123', csrf_token: mask_token(new_token) }) + context 'when the access type is access_token' do + let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [Gitlab::Auth::K8S_PROXY_SCOPE]) } - expect(response).to have_gitlab_http_status(:bad_request) - end + it 'returns 200 when the user has access' do + deployment_project.add_member(user, :developer) - it 'returns 401 when session is not found' do - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id('abc') - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) }) + send_request(params: { agent_id: agent.id, access_type: 'personal_access_token', access_key: personal_access_token.token }) - expect(response).to have_gitlab_http_status(:unauthorized) - end + expect(response).to have_gitlab_http_status(:success) + end - it 'returns 401 when CSRF token does not match' do - public_id = stub_user_session(user, new_token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) }) + it 'returns 400 when the feature flag is disabled' do + deployment_project.add_member(user, :developer) + stub_feature_flags(k8s_proxy_pat: false) - expect(response).to have_gitlab_http_status(:unauthorized) - end + send_request(params: { agent_id: agent.id, access_type: 'personal_access_token', access_key: personal_access_token.token }) - it 'returns 404 for non-existent agent' do - token = new_token - public_id = stub_user_session(user, token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: non_existing_record_id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + expect(response).to have_gitlab_http_status(:bad_request) + end - expect(response).to have_gitlab_http_status(:not_found) - end + it 'returns 403 when user has no access' do + send_request(params: { agent_id: agent.id, access_type: 'personal_access_token', access_key: personal_access_token.token }) - it 'returns 403 when user has no access' do - token = new_token - public_id = stub_user_session(user, token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + expect(response).to have_gitlab_http_status(:forbidden) + end - expect(response).to have_gitlab_http_status(:forbidden) - end + it 'returns 403 when user has incorrect token scope' do + personal_access_token.update!(scopes: [Gitlab::Auth::READ_API_SCOPE]) + deployment_project.add_member(user, :developer) - it 'returns 200 when user has access' do - deployment_project.add_member(user, :developer) - token = new_token - public_id = stub_user_session(user, token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + send_request(params: { agent_id: agent.id, access_type: 'personal_access_token', access_key: personal_access_token.token }) - expect(response).to have_gitlab_http_status(:success) - end + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 403 when user has no access to requested agent' do + deployment_project.add_member(user, :developer) + + send_request(params: { agent_id: another_agent.id, access_type: 'personal_access_token', access_key: personal_access_token.token }) - it 'returns 401 when user has valid KAS cookie and CSRF token but has no access to requested agent' do - deployment_project.add_member(user, :developer) - token = new_token - public_id = stub_user_session(user, token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: another_agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 404 for non-existent agent' do + send_request(params: { agent_id: non_existing_record_id, access_type: 'personal_access_token', access_key: personal_access_token.token }) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) + end end - it 'returns 401 when user id is not found in session' do - deployment_project.add_member(user, :developer) - token = new_token - public_id = stub_user_session_with_no_user_id(user, token) - access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) - send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + context 'when the access type is session_cookie' do + it 'returns 400 when cookie is invalid' do + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: '123', csrf_token: mask_token(new_token) }) - expect(response).to have_gitlab_http_status(:unauthorized) + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 401 when session is not found' do + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id('abc') + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 401 when CSRF token does not match' do + public_id = stub_user_session(user, new_token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 404 for non-existent agent' do + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: non_existing_record_id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 403 when user has no access' do + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 200 when user has access' do + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns 401 when user has valid KAS cookie and CSRF token but has no access to requested agent' do + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: another_agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 401 when user id is not found in session' do + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session_with_no_user_id(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end end end end diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb deleted file mode 100644 index b6e765eba37..00000000000 --- a/spec/requests/groups/email_campaigns_controller_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Groups::EmailCampaignsController, feature_category: :navigation do - using RSpec::Parameterized::TableSyntax - - describe 'GET #index', :snowplow do - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, group: group) } - let_it_be(:user) { create(:user) } - - let(:track) { 'create' } - let(:series) { '0' } - let(:schema) { described_class::EMAIL_CAMPAIGNS_SCHEMA_URL } - let(:subject_line_text) { Gitlab::Email::Message::InProductMarketing.for(track.to_sym).new(group: group, user: user, series: series.to_i).subject_line } - let(:data) do - { - namespace_id: group.id, - track: track.to_sym, - series: series.to_i, - subject_line: subject_line_text - } - end - - before do - sign_in(user) - group.add_developer(user) - end - - subject do - get group_email_campaigns_url(group, track: track, series: series) - response - end - - shared_examples 'track and redirect' do - it 'redirects' do - expect(subject).to have_gitlab_http_status(:redirect) - end - - context 'on SaaS', :saas do - it 'emits a snowplow event', :snowplow do - subject - - expect_snowplow_event( - category: described_class.name, - action: 'click', - context: [{ - schema: described_class::EMAIL_CAMPAIGNS_SCHEMA_URL, - data: { namespace_id: group.id, series: series.to_i, subject_line: subject_line_text, track: track.to_s } - }], - user: user, - namespace: group - ) - end - - it 'does not save the cta_click' do - expect(Users::InProductMarketingEmail).not_to receive(:save_cta_click) - - subject - end - end - - context 'when not on.com' do - it 'saves the cta_click' do - expect(Users::InProductMarketingEmail).to receive(:save_cta_click) - - subject - end - - it 'does not track snowplow events' do - subject - - expect_no_snowplow_event - end - end - end - - shared_examples 'no track and 404' do - it 'returns 404' do - expect(subject).to have_gitlab_http_status(:not_found) - end - - it 'does not emit a snowplow event', :snowplow do - subject - - expect_no_snowplow_event - end - end - - describe 'track parameter' do - context 'when valid' do - where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience)) - - with_them do - it_behaves_like 'track and redirect' - end - end - - context 'when invalid' do - where(track: [nil, 'xxxx']) - - with_them do - it_behaves_like 'no track and 404' - end - end - end - - describe 'series parameter' do - context 'when valid' do - where(series: (0..Namespaces::InProductMarketingEmailsService::TRACKS[:create][:interval_days].length - 1).to_a) - - with_them do - it_behaves_like 'track and redirect' - end - end - - context 'when invalid' do - where(series: [-1, nil, Namespaces::InProductMarketingEmailsService::TRACKS[:create][:interval_days].length]) - - with_them do - it_behaves_like 'no track and 404' - end - end - end - end -end diff --git a/spec/serializers/activity_pub/activity_streams_serializer_spec.rb b/spec/serializers/activity_pub/activity_streams_serializer_spec.rb new file mode 100644 index 00000000000..c74beba7a81 --- /dev/null +++ b/spec/serializers/activity_pub/activity_streams_serializer_spec.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::ActivityStreamsSerializer, feature_category: :integrations do + let(:implementer_class) do + Class.new(described_class) do + include WithPagination + end + end + + let(:entity_class) do + Class.new(Grape::Entity) do + expose :id do |*| + 'https://example.com/unique/url' + end + + expose :type do |*| + 'Person' + end + + expose :name do |*| + 'Alice' + end + end + end + + shared_examples_for 'ActivityStreams document' do + it 'belongs to the ActivityStreams namespace' do + expect(subject['@context']).to eq 'https://www.w3.org/ns/activitystreams' + end + + it 'has a unique identifier' do + expect(subject).to have_key 'id' + end + + it 'has a type' do + expect(subject).to have_key 'type' + end + end + + before do + implementer_class.entity entity_class + end + + context 'when the serializer is not paginated' do + let(:resource) { build_stubbed(:release) } + let(:outbox_url) { 'https://example.com/unique/url/outbox' } + + context 'with a valid represented entity' do + subject { implementer_class.new.represent(resource, outbox: outbox_url) } + + it_behaves_like 'ActivityStreams document' + + it 'exposes an outbox' do + expect(subject['outbox']).to eq 'https://example.com/unique/url/outbox' + end + + it 'includes serialized data' do + expect(subject['name']).to eq 'Alice' + end + end + + context 'when the represented entity provides no identifier' do + subject { implementer_class.new.represent(resource, outbox: outbox_url) } + + before do + allow(entity_class).to receive(:represent).and_return({ type: 'Person' }) + end + + it 'raises an exception' do + expect { subject }.to raise_error(ActivityPub::ActivityStreamsSerializer::MissingIdentifierError) + end + end + + context 'when the represented entity provides no type' do + subject { implementer_class.new.represent(resource, outbox: outbox_url) } + + before do + allow(entity_class).to receive(:represent).and_return({ id: 'https://example.com/unique/url' }) + end + + it 'raises an exception' do + expect { subject }.to raise_error(ActivityPub::ActivityStreamsSerializer::MissingTypeError) + end + end + + context 'when the caller provides no outbox parameter' do + subject { implementer_class.new.represent(resource) } + + it 'raises an exception' do + expect { subject }.to raise_error(ActivityPub::ActivityStreamsSerializer::MissingOutboxError) + end + end + end + + context 'when the serializer is paginated' do + let(:resources) { build_stubbed_list(:release, 3) } + let(:request) { ActionDispatch::Request.new(request_data) } + let(:response) { ActionDispatch::Response.new } + let(:url) { 'https://example.com/resource/url' } + let(:decorated) { implementer_class.new.with_pagination(request, response) } + + before do + allow(resources).to receive(:page).and_return(resources) + allow(resources).to receive(:per).and_return(resources) + allow(resources).to receive(:current_page).and_return(2) + allow(resources).to receive(:total_pages).and_return(3) + allow(resources).to receive(:total_count).and_return(10) + allow(decorated.paginator).to receive(:paginate).and_return(resources) + end + + context 'when no page parameter is provided' do + subject { decorated.represent(resources) } + + let(:request_data) do + { "rack.url_scheme" => "https", "HTTP_HOST" => "example.com", "PATH_INFO" => '/resource/url' } + end + + it_behaves_like 'ActivityStreams document' + + it 'is an index document for the pagination' do + expect(subject['type']).to eq 'OrderedCollection' + end + + it 'contains the total amount of items' do + expect(subject['totalItems']).to eq 10 + end + + it 'contains links to first and last page' do + expect(subject['first']).to eq "#{url}?page=1" + expect(subject['last']).to eq "#{url}?page=3" + end + end + + context 'when a page parameter is provided' do + subject { decorated.represent(resources) } + + let(:request_data) do + { 'rack.url_scheme' => 'https', 'HTTP_HOST' => 'example.com', 'PATH_INFO' => '/resource/url', + 'QUERY_STRING' => 'page=2&per_page=1' } + end + + it_behaves_like 'ActivityStreams document' + + it 'is a page document' do + expect(subject['type']).to eq 'OrderedCollectionPage' + end + + it 'contains navigation links' do + expect(subject['prev']).to be_present + expect(subject['next']).to be_present + expect(subject['partOf']).to be_present + end + end + end +end diff --git a/spec/serializers/activity_pub/project_entity_spec.rb b/spec/serializers/activity_pub/project_entity_spec.rb new file mode 100644 index 00000000000..f273acace73 --- /dev/null +++ b/spec/serializers/activity_pub/project_entity_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::ProjectEntity, feature_category: :groups_and_projects do + let(:project) { build_stubbed(:project, name: 'Fooify', path: 'fooify') } + let(:entity) { described_class.new(project) } + + context 'as json' do + subject { entity.as_json } + + it 'has releases page as id' do + expect(subject[:id]).to match(%r{/fooify$}) + end + + it 'is an Application actor' do + expect(subject[:type]).to eq 'Application' + end + + it 'provides project name' do + expect(subject[:name]).to eq project.name + end + + it 'provides a description of the project' do + expect(subject[:summary]).to eq project.description + end + + it 'provides an url for web content' do + expect(subject[:url]).to match(%r{/fooify$}) + end + end +end diff --git a/spec/serializers/activity_pub/release_entity_spec.rb b/spec/serializers/activity_pub/release_entity_spec.rb new file mode 100644 index 00000000000..a473fbcc2bd --- /dev/null +++ b/spec/serializers/activity_pub/release_entity_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::ReleaseEntity, feature_category: :groups_and_projects do + let(:release) { build_stubbed(:release) } + let(:entity) { described_class.new(release, url: '/outbox') } + + context 'as json' do + subject { entity.as_json } + + it 'has tag as id' do + expect(subject[:id]).to match(/##{release.tag}$/) + end + + it 'is a Create activity' do + expect(subject[:type]).to eq 'Create' + end + + it 'is addressed to public' do + expect(subject[:to]).to eq 'https://www.w3.org/ns/activitystreams#Public' + end + + it 'has an author' do + expect(subject[:actor]).to include(:id, :type, :name, :preferredUsername, :url) + end + + it 'embeds the release as an Application actor' do + expect(subject[:object][:type]).to eq 'Application' + end + + it 'provides release name' do + expect(subject[:object][:name]).to eq release.name + end + + it 'provides release description' do + expect(subject[:object][:content]).to eq release.description + end + + it 'provides an url for web content' do + expect(subject[:object][:url]).to include release.tag + end + + it 'provides project data as context' do + expect(subject[:object][:context]).to include(:id, :type, :name, :summary, :url) + end + end +end diff --git a/spec/serializers/activity_pub/releases_actor_entity_spec.rb b/spec/serializers/activity_pub/releases_actor_entity_spec.rb new file mode 100644 index 00000000000..fe388968867 --- /dev/null +++ b/spec/serializers/activity_pub/releases_actor_entity_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::ReleasesActorEntity, feature_category: :groups_and_projects do + let(:project) { build_stubbed(:project, name: 'Fooify', path: 'fooify') } + let(:releases) { build_stubbed_list(:release, 3, project: project) } + + let(:entity) { described_class.new(project) } + + context 'as json' do + subject { entity.as_json } + + it 'has releases page as id' do + expect(subject[:id]).to include "/fooify/-/releases" + end + + it 'is an Application actor' do + expect(subject[:type]).to eq 'Application' + end + + it 'has a recognizable username' do + expect(subject[:preferredUsername]).to include 'releases' + end + + it 'has a recognizable full name' do + expect(subject[:name]).to eq 'Releases - Fooify' + end + + it 'provides a description of the project' do + expect(subject[:content]).to eq project.description + end + + it 'provides project data as context' do + expect(subject[:context]).to include(:id, :type, :name, :summary, :url) + expect(subject[:context][:id]).to match(%r{/fooify$}) + end + end +end diff --git a/spec/serializers/activity_pub/releases_actor_serializer_spec.rb b/spec/serializers/activity_pub/releases_actor_serializer_spec.rb new file mode 100644 index 00000000000..bc754eabe5c --- /dev/null +++ b/spec/serializers/activity_pub/releases_actor_serializer_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::ReleasesActorSerializer, feature_category: :groups_and_projects do + let(:project) { build_stubbed(:project, name: 'Fooify', path: 'fooify') } + let(:releases) { build_stubbed_list(:release, 3, project: project) } + + context 'when there is a single object provided' do + subject { described_class.new.represent(project, outbox: '/outbox') } + + it 'serializes the actor attributes' do + expect(subject).to include(:id, :type, :preferredUsername, :name, :content, :context) + end + end +end diff --git a/spec/serializers/activity_pub/releases_outbox_serializer_spec.rb b/spec/serializers/activity_pub/releases_outbox_serializer_spec.rb new file mode 100644 index 00000000000..606b0130e0f --- /dev/null +++ b/spec/serializers/activity_pub/releases_outbox_serializer_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::ReleasesOutboxSerializer, feature_category: :groups_and_projects do + let(:decorated) { described_class.new.with_pagination(request, response) } + + let(:project) { build_stubbed(:project, name: 'Fooify', path: 'fooify') } + let(:releases) { build_stubbed_list(:release, 3, project: project) } + + before do + allow(releases).to receive(:page).and_return(releases) + allow(releases).to receive(:per).and_return(releases) + allow(releases).to receive(:current_page).and_return(1) + allow(releases).to receive(:total_pages).and_return(1) + allow(decorated.paginator).to receive(:paginate).and_return(releases) + end + + context 'when there is a list of objects provided' do + subject { decorated.represent(releases, url: '/outbox') } + + let(:request) { ActionDispatch::Request.new({ 'QUERY_STRING' => 'page=1' }) } + let(:response) { ActionDispatch::Response.new } + + it 'is a OrderedCollection document' do + expect(subject[:type]).to eq 'OrderedCollectionPage' + end + + it 'serializes the releases' do + expect(subject[:orderedItems].count).to eq 3 + expect(subject[:orderedItems][0]).to include(:id, :type, :to, :actor, :object) + end + end +end diff --git a/spec/serializers/activity_pub/user_entity_spec.rb b/spec/serializers/activity_pub/user_entity_spec.rb new file mode 100644 index 00000000000..d9ab7a11ecf --- /dev/null +++ b/spec/serializers/activity_pub/user_entity_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ActivityPub::UserEntity, feature_category: :user_profile do + let(:user) { build_stubbed(:user, name: 'Alice', username: 'alice') } + let(:entity) { described_class.new(user) } + + context 'as json' do + subject { entity.as_json } + + it 'has releases page as id' do + expect(subject[:id]).to match(%r{/alice$}) + end + + it 'is a Person actor' do + expect(subject[:type]).to eq 'Person' + end + + it 'provides project name' do + expect(subject[:name]).to eq 'Alice' + end + + it 'provides an url for web content' do + expect(subject[:url]).to match(%r{/alice$}) + end + end +end diff --git a/spec/services/ci/create_commit_status_service_spec.rb b/spec/services/ci/create_commit_status_service_spec.rb new file mode 100644 index 00000000000..ec200e24c8f --- /dev/null +++ b/spec/services/ci/create_commit_status_service_spec.rb @@ -0,0 +1,461 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::CreateCommitStatusService, :clean_gitlab_redis_cache, feature_category: :continuous_integration do + using RSpec::Parameterized::TableSyntax + + subject(:response) { execute_service(params) } + + let_it_be_with_refind(:project) { create(:project, :repository) } + let_it_be(:commit) { project.repository.commit } + let_it_be(:guest) { create_user(:guest) } + let_it_be(:reporter) { create_user(:reporter) } + let_it_be(:developer) { create_user(:developer) } + + let(:user) { developer } + let(:sha) { commit.id } + let(:params) { { state: 'pending' } } + let(:job) { response.payload[:job] } + + %w[pending running success failed canceled].each do |status| + context "for #{status}" do + let(:params) { { state: status } } + + context 'when pipeline for sha does not exists' do + it 'creates commit status and sets pipeline iid' do + expect(response).to be_success + expect(job.sha).to eq(commit.id) + expect(job.status).to eq(status) + expect(job.name).to eq('default') + expect(job.ref).not_to be_empty + expect(job.target_url).to be_nil + expect(job.description).to be_nil + expect(job.pipeline_id).not_to be_nil + + expect(CommitStatus.find(job.id)).to be_api_failure if status == 'failed' + + expect(::Ci::Pipeline.last.iid).not_to be_nil + end + end + end + end + + context 'when status transitions from pending' do + before do + execute_service(state: 'pending') + end + + %w[running success failed canceled].each do |status| + context "for #{status}" do + let(:params) { { state: status } } + + it "changes to #{status}" do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(1) + .and not_change { ::Ci::Stage.count }.from(1) + .and not_change { ::CommitStatus.count }.from(1) + + expect(response).to be_success + expect(job.status).to eq(status) + end + end + end + + context 'for invalid transition' do + let(:params) { { state: 'pending' } } + + it 'returns bad request and error message' do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(1) + .and not_change { ::Ci::Stage.count }.from(1) + .and not_change { ::CommitStatus.count }.from(1) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq( + "Cannot transition status via :enqueue from :pending (Reason(s): Status cannot transition via \"enqueue\")" + ) + end + end + end + + context 'with all optional parameters' do + context 'when creating a commit status' do + let(:params) do + { + sha: sha, + state: 'success', + context: 'coverage', + ref: 'master', + description: 'test', + coverage: 80.0, + target_url: 'http://gitlab.com/status' + } + end + + it 'creates commit status' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(1) + + expect(response).to be_success + expect(job.sha).to eq(commit.id) + expect(job.status).to eq('success') + expect(job.name).to eq('coverage') + expect(job.ref).to eq('master') + expect(job.coverage).to eq(80.0) + expect(job.description).to eq('test') + expect(job.target_url).to eq('http://gitlab.com/status') + end + + context 'when merge request exists for given branch' do + let!(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'develop') + end + + it 'sets head pipeline' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(1) + + expect(response).to be_success + expect(merge_request.reload.head_pipeline).not_to be_nil + end + end + end + + context 'when updating a commit status' do + let(:parameters) do + { + state: 'success', + name: 'coverage', + ref: 'master' + } + end + + let(:updatable_optional_attributes) do + { + description: 'new description', + coverage: 90.0 + } + end + + let(:params) { parameters.merge(updatable_optional_attributes) } + + # creating the initial commit status + before do + execute_service( + sha: sha, + state: 'running', + context: 'coverage', + ref: 'master', + description: 'coverage test', + coverage: 10.0, + target_url: 'http://gitlab.com/status' + ) + end + + it 'updates a commit status' do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(1) + .and not_change { ::Ci::Stage.count }.from(1) + .and not_change { ::CommitStatus.count }.from(1) + + expect(response).to be_success + expect(job.sha).to eq(commit.id) + expect(job.status).to eq('success') + expect(job.name).to eq('coverage') + expect(job.ref).to eq('master') + expect(job.coverage).to eq(90.0) + expect(job.description).to eq('new description') + expect(job.target_url).to eq('http://gitlab.com/status') + end + + context 'when the `state` parameter is sent the same' do + let(:parameters) do + { + sha: sha, + state: 'running', + name: 'coverage', + ref: 'master' + } + end + + it 'does not update the commit status' do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(1) + .and not_change { ::Ci::Stage.count }.from(1) + .and not_change { ::CommitStatus.count }.from(1) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq( + "Cannot transition status via :run from :running (Reason(s): Status cannot transition via \"run\")" + ) + + commit_status = project.commit_statuses.find_by!(name: 'coverage') + + expect(commit_status.description).to eq('coverage test') + expect(commit_status.coverage).to eq(10.0) + end + end + end + + context 'when a pipeline id is specified' do + let!(:first_pipeline) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end + + let!(:other_pipeline) do + project.ci_pipelines.build(source: :push, sha: commit.id, ref: 'master', status: 'created').tap do |p| + p.ensure_project_iid! # Necessary to avoid cross-database modification error + p.save! + end + end + + let(:params) do + { + sha: sha, + pipeline_id: other_pipeline.id, + state: 'success', + ref: 'master' + } + end + + it 'update the correct pipeline', :sidekiq_might_not_need_inline do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(2) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(1) + + expect(first_pipeline.reload.status).to eq('created') + expect(other_pipeline.reload.status).to eq('success') + end + end + end + + context 'when retrying a commit status' do + subject(:response) do + execute_service(state: 'failed', name: 'test', ref: 'master') + + execute_service(state: 'success', name: 'test', ref: 'master') + end + + it 'correctly posts a new commit status' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(2) + + expect(response).to be_success + expect(job.sha).to eq(commit.id) + expect(job.status).to eq('success') + end + + it 'retries the commit status', :sidekiq_might_not_need_inline do + response + + expect(CommitStatus.count).to eq 2 + expect(CommitStatus.first).to be_retried + expect(CommitStatus.last.pipeline).to be_success + end + end + + context 'when status is invalid' do + let(:params) { { state: 'invalid' } } + + it 'does not create commit status' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and not_change { ::CommitStatus.count }.from(0) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq('invalid state') + end + end + + context 'when request without a state made' do + let(:params) { {} } + + it 'does not create commit status' do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(0) + .and not_change { ::Ci::Stage.count }.from(0) + .and not_change { ::CommitStatus.count }.from(0) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message).to eq('State is required') + end + end + + context 'when updating a protected ref' do + let(:params) { { state: 'running', ref: 'master' } } + + before do + create(:protected_branch, project: project, name: 'master') + end + + context 'with user as developer' do + let(:user) { developer } + + it 'does not create commit status' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and not_change { ::Ci::Stage.count }.from(0) + .and not_change { ::CommitStatus.count }.from(0) + + expect(response).to be_error + expect(response.http_status).to eq(:forbidden) + expect(response.message).to eq('403 Forbidden') + end + end + + context 'with user as maintainer' do + let(:user) { create_user(:maintainer) } + + it 'creates commit status' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(1) + + expect(response).to be_success + end + end + end + + context 'when commit SHA is invalid' do + let(:sha) { 'invalid_sha' } + let(:params) { { state: 'running', sha: sha } } + + it 'returns not found error' do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(0) + .and not_change { ::Ci::Stage.count }.from(0) + .and not_change { ::CommitStatus.count }.from(0) + + expect(response).to be_error + expect(response.http_status).to eq(:not_found) + expect(response.message).to eq('404 Commit Not Found') + end + end + + context 'when target URL is an invalid address' do + let(:params) { { state: 'pending', target_url: 'invalid url' } } + + it 'responds with bad request status and validation errors' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and not_change { ::CommitStatus.count }.from(0) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message[:target_url]) + .to include 'is blocked: Only allowed schemes are http, https' + end + end + + context 'when target URL is an unsupported scheme' do + let(:params) { { state: 'pending', target_url: 'git://example.com' } } + + it 'responds with bad request status and validation errors' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and not_change { ::CommitStatus.count }.from(0) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message[:target_url]) + .to include 'is blocked: Only allowed schemes are http, https' + end + end + + context 'when trying to update a status of a different type' do + let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') } + let!(:ci_build) { create(:ci_build, pipeline: pipeline, name: 'test-job') } + let(:params) { { state: 'pending', name: 'test-job' } } + + before do + execute_service(params) + end + + it 'responds with bad request status and validation errors' do + expect { response } + .to not_change { ::Ci::Pipeline.count }.from(1) + .and not_change { ::Ci::Stage.count }.from(2) + .and not_change { ::CommitStatus.count }.from(1) + + expect(response).to be_error + expect(response.http_status).to eq(:bad_request) + expect(response.message[:name]) + .to include 'has already been taken' + end + end + + context 'with partitions', :ci_partitionable do + let(:current_partition_id) { ci_testing_partition_id } + let(:params) { { state: 'running' } } + + before do + allow(Ci::Pipeline) + .to receive(:current_partition_value) { current_partition_id } + end + + it 'creates records in the current partition' do + expect { response } + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(1) + + expect(response).to be_success + + status = CommitStatus.find(job.id) + + expect(status.partition_id).to eq(current_partition_id) + expect(status.pipeline.partition_id).to eq(current_partition_id) + end + end + + context 'for race condition' do + let(:licenses_snyk_params) { { state: 'running', name: 'licenses', description: 'testing' } } + let(:security_snyk_params) { { state: 'running', name: 'security', description: 'testing' } } + let(:snyk_params_list) { [licenses_snyk_params, security_snyk_params] } + + it 'creates one pipeline and two jobs (one for licenses, one for security)' do + expect do + snyk_params_list.map do |snyk_params| + Thread.new do + response = execute_service(snyk_params) + expect(response).to be_success + end + end.each(&:join) + end + .to change { ::Ci::Pipeline.count }.by(1) + .and change { ::Ci::Stage.count }.by(1) + .and change { ::CommitStatus.count }.by(2) + end + end + + def create_user(access_level_trait) + user = create(:user) + create(:project_member, access_level_trait, user: user, project: project) + user + end + + def execute_service(params = self.params) + described_class + .new(project, user, params) + .execute(optional_commit_status_params: params.slice(*%i[target_url description coverage])) + end +end diff --git a/spec/services/concerns/services/return_service_responses_spec.rb b/spec/services/concerns/services/return_service_responses_spec.rb new file mode 100644 index 00000000000..3589b952e87 --- /dev/null +++ b/spec/services/concerns/services/return_service_responses_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Services::ReturnServiceResponses, feature_category: :rate_limiting do + subject(:object) { Class.new { include Services::ReturnServiceResponses }.new } + + let(:message) { 'a delivering message' } + let(:payload) { 'string payload' } + + describe '#success' do + it 'returns a ServiceResponse instance' do + response = object.success(payload) + expect(response).to be_an(ServiceResponse) + expect(response).to be_success + expect(response.message).to be_nil + expect(response.payload).to eq(payload) + expect(response.http_status).to eq(:ok) + end + end + + describe '#error' do + it 'returns a ServiceResponse instance' do + response = object.error(message, :not_found, pass_back: payload) + expect(response).to be_an(ServiceResponse) + expect(response).to be_error + expect(response.message).to eq(message) + expect(response.payload).to eq(payload) + expect(response.http_status).to eq(:not_found) + end + end +end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 1c5dde96b62..6ac1a27745f 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -8573,7 +8573,6 @@ - './spec/requests/groups/crm/contacts_controller_spec.rb' - './spec/requests/groups/crm/organizations_controller_spec.rb' - './spec/requests/groups/deploy_tokens_controller_spec.rb' -- './spec/requests/groups/email_campaigns_controller_spec.rb' - './spec/requests/groups/harbor/artifacts_controller_spec.rb' - './spec/requests/groups/harbor/repositories_controller_spec.rb' - './spec/requests/groups/harbor/tags_controller_spec.rb' diff --git a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb index e61c884cd2b..14d0ac81250 100644 --- a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb +++ b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb @@ -121,8 +121,7 @@ RSpec.shared_examples 'every metric definition' do let(:ignored_classes) do [ Gitlab::Usage::Metrics::Instrumentations::IssuesWithAlertManagementAlertsMetric, - Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents, - Gitlab::Usage::Metrics::Instrumentations::IssuesWithSelfManagedPrometheusAlertEvents + Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents ].freeze end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb index ec2ae0b8a73..4eae8632467 100644 --- a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do let(:job) { { 'args' => [1, {}, 'key'], 'jid' => 'jid' } } it 'notifies the waiter' do - expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) end @@ -23,7 +23,7 @@ RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do specify do allow_next(worker.importer_class).to receive(:execute) - expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything) + expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything, ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project_id, {}, waiter_key) end diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb index 3b7bbfc8a7b..27e1077b138 100644 --- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb @@ -273,7 +273,8 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures, featur .to receive(:notify) .with( job['args'].last, - job['jid'] + job['jid'], + ttl: Gitlab::Import::JOB_WAITER_TTL ) sidekiq_retries_exhausted diff --git a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb index 6475be0243c..c76ce6b555f 100644 --- a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb @@ -110,7 +110,7 @@ RSpec.describe Gitlab::GithubImport::ReschedulingMethods, feature_category: :imp expect(Gitlab::JobWaiter) .to receive(:notify) - .with('123', 'abc123') + .with('123', 'abc123', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.notify_waiter('123') end diff --git a/spec/workers/concerns/gitlab/notify_upon_death_spec.rb b/spec/workers/concerns/gitlab/import/notify_upon_death_spec.rb index 36faf3ee296..1f760e8542b 100644 --- a/spec/workers/concerns/gitlab/notify_upon_death_spec.rb +++ b/spec/workers/concerns/gitlab/import/notify_upon_death_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do +RSpec.describe Gitlab::Import::NotifyUponDeath, feature_category: :importers do let(:worker_class) do Class.new do include Sidekiq::Worker - include Gitlab::NotifyUponDeath + include Gitlab::Import::NotifyUponDeath end end @@ -16,13 +16,13 @@ RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do expect(Gitlab::JobWaiter) .to receive(:notify) - .with('123abc', '123') + .with('123abc', '123', ttl: Gitlab::Import::JOB_WAITER_TTL) worker_class.sidekiq_retries_exhausted_block.call(job) end it 'does not notify the JobWaiter when only 2 arguments are given' do - job = { 'args' => [12, {}], 'jid' => '123' } + job = { 'args' => [12, '123abc'], 'jid' => '123' } expect(Gitlab::JobWaiter) .not_to receive(:notify) @@ -31,7 +31,7 @@ RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do end it 'does not notify the JobWaiter when only 1 argument is given' do - job = { 'args' => [12], 'jid' => '123' } + job = { 'args' => ['123abc'], 'jid' => '123' } expect(Gitlab::JobWaiter) .not_to receive(:notify) diff --git a/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb index dd3235f846c..376078532cd 100644 --- a/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb +++ b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_c end it 'notifies job waiter' do - expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project.id, {}, job_waiter_key) end @@ -44,7 +44,7 @@ RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_c context 'when project does not exists' do it 'does not call importer and notifies job waiter' do expect(importer_class).not_to receive(:new) - expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(-1, {}, job_waiter_key) end @@ -55,7 +55,7 @@ RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_c project = create(:project, :import_canceled) expect(importer_class).not_to receive(:new) - expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project.id, {}, job_waiter_key) end diff --git a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb index 2e89263bcf3..dc715c3026b 100644 --- a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb +++ b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb @@ -68,7 +68,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i .to receive(:info) .with(log_attributes.merge('message' => 'start importer')) expect(importer).to receive(:execute).and_return(importer_result) - expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid) + expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL) expect(Gitlab::GithubImport::Logger) .to receive(:info) .with(log_attributes.merge('message' => 'importer finished')) @@ -114,7 +114,9 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i expect(Gitlab::GithubImport::Logger) .to receive(:error) .with(log_attributes.merge('message' => 'importer failed', 'error.message' => 'error_message')) - expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid) + expect(Gitlab::JobWaiter) + .to receive(:notify) + .with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL) subject.perform(user.id, gist_hash, 'some_key') @@ -189,7 +191,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i it 'notifies the JobWaiter' do expect(Gitlab::JobWaiter) .to receive(:notify) - .with(job['args'].last, job['jid']) + .with(job['args'].last, job['jid'], ttl: Gitlab::Import::JOB_WAITER_TTL) sidekiq_retries_exhausted end diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb index 121f30ea9d5..2faa28a365e 100644 --- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb +++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb @@ -35,6 +35,17 @@ RSpec.describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_sha worker.perform(project.id, { '123' => 2 }, :finish) end + + context 'when import state is nil' do + let(:import_state) { nil } + + it 'clears the JobWaiter cache and does not perform any work' do + expect(Gitlab::JobWaiter).to receive(:delete_key).with('123') + expect(worker).not_to receive(:wait_for_jobs) + + worker.perform(project.id, { '123' => 2 }, :finish) + end + end end context 'when there are no remaining jobs' do diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb index 5209395923f..6dfab44b228 100644 --- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb +++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb @@ -12,9 +12,9 @@ RSpec.describe Gitlab::JiraImport::ImportIssueWorker, feature_category: :importe describe 'modules' do it { expect(described_class).to include_module(ApplicationWorker) } - it { expect(described_class).to include_module(Gitlab::NotifyUponDeath) } it { expect(described_class).to include_module(Gitlab::JiraImport::QueueOptions) } it { expect(described_class).to include_module(Gitlab::Import::DatabaseHelpers) } + it { expect(described_class).to include_module(Gitlab::Import::NotifyUponDeath) } end subject { described_class.new } |