diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-14 15:08:59 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-14 15:08:59 +0000 |
commit | 9b762f50fee09b50b97b5ab208a9a62522447c8c (patch) | |
tree | 4dbd16c66f6aeacc1b88c1e3350df09ce4f91183 /spec | |
parent | 9769ccf613ec45634ee32efaf1c39763a759a917 (diff) | |
download | gitlab-ce-9b762f50fee09b50b97b5ab208a9a62522447c8c.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
20 files changed, 833 insertions, 62 deletions
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb index 011528016ce..1b4b67eeaff 100644 --- a/spec/controllers/profiles/accounts_controller_spec.rb +++ b/spec/controllers/profiles/accounts_controller_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Profiles::AccountsController do end end - [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq, :dingtalk].each do |provider| + [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq, :dingtalk, :alicloud].each do |provider| describe "#{provider} provider" do let(:user) { create(:omniauth_user, provider: provider.to_s) } diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 9482448fc03..4abcd414e51 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -211,6 +211,7 @@ RSpec.describe SearchController do :global_search_merge_requests_tab | 'merge_requests' :global_search_wiki_tab | 'wiki_blobs' :global_search_commits_tab | 'commits' + :global_search_users_tab | 'users' end with_them do diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 8442c214cd3..ffcd759435c 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -701,6 +701,24 @@ RSpec.describe UploadsController do end end end + + context 'when viewing alert metric images' do + let!(:user) { create(:user) } + let!(:project) { create(:project) } + let(:alert) { create(:alert_management_alert, project: project) } + let(:metric_image) { create(:alert_metric_image, alert: alert) } + + before do + project.add_developer(user) + sign_in(user) + end + + it "responds with status 200" do + get :show, params: { model: "alert_management_metric_image", mounted_as: 'file', id: metric_image.id, filename: metric_image.filename } + + expect(response).to have_gitlab_http_status(:ok) + end + end end def post_authorize(verified: true) diff --git a/spec/factories/alert_management/metric_images.rb b/spec/factories/alert_management/metric_images.rb new file mode 100644 index 00000000000..d7d8182af3e --- /dev/null +++ b/spec/factories/alert_management/metric_images.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :alert_metric_image, class: 'AlertManagement::MetricImage' do + association :alert, factory: :alert_management_alert + url { generate(:url) } + + trait :local do + file_store { ObjectStorage::Store::LOCAL } + end + + after(:build) do |image| + image.file = fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') + end + end +end diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 93674057fed..ea5bb8c33b2 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'OAuth Login', :js, :allow_forgery_protection do end providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, - :facebook, :cas3, :auth0, :authentiq, :salesforce, :dingtalk] + :facebook, :cas3, :auth0, :authentiq, :salesforce, :dingtalk, :alicloud] around do |example| with_omniauth_full_host { example.run } diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb index e253d03b411..879ffd2932b 100644 --- a/spec/features/projects/wikis_spec.rb +++ b/spec/features/projects/wikis_spec.rb @@ -8,26 +8,14 @@ RSpec.describe 'Project wikis', :js do let(:wiki) { create(:project_wiki, user: user, project: project) } let(:project) { create(:project, namespace: user.namespace, creator: user) } - shared_examples 'wiki feature tests' do - it_behaves_like 'User creates wiki page' - it_behaves_like 'User deletes wiki page' - it_behaves_like 'User previews wiki changes' - it_behaves_like 'User updates wiki page' - it_behaves_like 'User uses wiki shortcuts' - it_behaves_like 'User views AsciiDoc page with includes' - it_behaves_like 'User views a wiki page' - it_behaves_like 'User views wiki pages' - it_behaves_like 'User views wiki sidebar' - it_behaves_like 'User views Git access wiki page' - end - - it_behaves_like 'wiki feature tests' - - context 'when feature flag :wiki_async_load is disabled' do - before do - stub_feature_flags(wiki_async_load: false) - end - - it_behaves_like 'wiki feature tests' - end + it_behaves_like 'User creates wiki page' + it_behaves_like 'User deletes wiki page' + it_behaves_like 'User previews wiki changes' + it_behaves_like 'User updates wiki page' + it_behaves_like 'User uses wiki shortcuts' + it_behaves_like 'User views AsciiDoc page with includes' + it_behaves_like 'User views a wiki page' + it_behaves_like 'User views wiki pages' + it_behaves_like 'User views wiki sidebar' + it_behaves_like 'User views Git access wiki page' end diff --git a/spec/frontend/content_editor/components/wrappers/image_spec.js b/spec/frontend/content_editor/components/wrappers/media_spec.js index 7b057f9cabc..3e95e2f3914 100644 --- a/spec/frontend/content_editor/components/wrappers/image_spec.js +++ b/spec/frontend/content_editor/components/wrappers/media_spec.js @@ -1,21 +1,24 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { NodeViewWrapper } from '@tiptap/vue-2'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ImageWrapper from '~/content_editor/components/wrappers/image.vue'; +import MediaWrapper from '~/content_editor/components/wrappers/media.vue'; -describe('content/components/wrappers/image', () => { +describe('content/components/wrappers/media', () => { let wrapper; const createWrapper = async (nodeAttrs = {}) => { - wrapper = shallowMountExtended(ImageWrapper, { + wrapper = shallowMountExtended(MediaWrapper, { propsData: { node: { attrs: nodeAttrs, + type: { + name: 'image', + }, }, }, }); }; - const findImage = () => wrapper.findByTestId('image'); + const findMedia = () => wrapper.findByTestId('media'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); afterEach(() => { @@ -33,7 +36,7 @@ describe('content/components/wrappers/image', () => { createWrapper({ src }); - expect(findImage().attributes().src).toBe(src); + expect(findMedia().attributes().src).toBe(src); }); describe('when uploading', () => { @@ -45,8 +48,8 @@ describe('content/components/wrappers/image', () => { expect(findLoadingIcon().exists()).toBe(true); }); - it('adds gl-opacity-5 class selector to image', () => { - expect(findImage().classes()).toContain('gl-opacity-5'); + it('adds gl-opacity-5 class selector to the media tag', () => { + expect(findMedia().classes()).toContain('gl-opacity-5'); }); }); @@ -59,8 +62,8 @@ describe('content/components/wrappers/image', () => { expect(findLoadingIcon().exists()).toBe(false); }); - it('does not add gl-opacity-5 class selector to image', () => { - expect(findImage().classes()).not.toContain('gl-opacity-5'); + it('does not add gl-opacity-5 class selector to the media tag', () => { + expect(findMedia().classes()).not.toContain('gl-opacity-5'); }); }); }); diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index ec67545cf17..9f035810b9e 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -1,7 +1,10 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; import Attachment from '~/content_editor/extensions/attachment'; import Image from '~/content_editor/extensions/image'; +import Audio from '~/content_editor/extensions/audio'; +import Video from '~/content_editor/extensions/video'; import Link from '~/content_editor/extensions/link'; import Loading from '~/content_editor/extensions/loading'; import { VARIANT_DANGER } from '~/flash'; @@ -14,6 +17,23 @@ const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="au <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"> </a> </p>`; + +const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto"> + <span class="media-container video-container"> + <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4"> + </video> + <a href="/himkp/test/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a> + </span> +</p>`; + +const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto"> + <span class="media-container audio-container"> + <audio src="/himkp/test/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3"> + </audio> + <a href="/himkp/test/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a> + </span> +</p>`; + const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto"> <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a> </p>`; @@ -23,6 +43,8 @@ describe('content_editor/extensions/attachment', () => { let doc; let p; let image; + let audio; + let video; let loading; let link; let renderMarkdown; @@ -31,15 +53,18 @@ describe('content_editor/extensions/attachment', () => { const uploadsPath = '/uploads/'; const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' }); + const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' }); + const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' }); const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' }); const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => { return new Promise((resolve) => { let counter = 1; - const handleTransaction = () => { + const handleTransaction = async () => { if (counter === number) { expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON()); tiptapEditor.off('update', handleTransaction); + await waitForPromises(); resolve(); } @@ -60,18 +85,22 @@ describe('content_editor/extensions/attachment', () => { Loading, Link, Image, + Audio, + Video, Attachment.configure({ renderMarkdown, uploadsPath, eventHub }), ], }); ({ - builders: { doc, p, image, loading, link }, + builders: { doc, p, image, audio, video, loading, link }, } = createDocBuilder({ tiptapEditor, names: { loading: { markType: Loading.name }, image: { nodeType: Image.name }, link: { nodeType: Link.name }, + audio: { nodeType: Audio.name }, + video: { nodeType: Video.name }, }, })); @@ -103,17 +132,22 @@ describe('content_editor/extensions/attachment', () => { tiptapEditor.commands.setContent(initialDoc.toJSON()); }); - describe('when the file has image mime type', () => { - const base64EncodedFile = 'data:image/png;base64,Zm9v'; + describe.each` + nodeType | mimeType | html | file | mediaType + ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)} + ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)} + ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)} + `('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => { + const base64EncodedFile = `data:${mimeType};base64,Zm9v`; beforeEach(() => { - renderMarkdown.mockResolvedValue(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML); + renderMarkdown.mockResolvedValue(html); }); describe('when uploading succeeds', () => { const successResponse = { link: { - markdown: '![test-file](test-file.png)', + markdown: `![test-file](${file.name})`, }, }; @@ -121,21 +155,21 @@ describe('content_editor/extensions/attachment', () => { mock.onPost().reply(httpStatus.OK, successResponse); }); - it('inserts an image with src set to the encoded image file and uploading true', async () => { - const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile }))); + it('inserts a media content with src set to the encoded content and uploading true', async () => { + const expectedDoc = doc(p(mediaType({ uploading: true, src: base64EncodedFile }))); await expectDocumentAfterTransaction({ number: 1, expectedDoc, - action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }), + action: () => tiptapEditor.commands.uploadAttachment({ file }), }); }); - it('updates the inserted image with canonicalSrc when upload is successful', async () => { + it('updates the inserted content with canonicalSrc when upload is successful', async () => { const expectedDoc = doc( p( - image({ - canonicalSrc: 'test-file.png', + mediaType({ + canonicalSrc: file.name, src: base64EncodedFile, alt: 'test-file', uploading: false, @@ -146,7 +180,7 @@ describe('content_editor/extensions/attachment', () => { await expectDocumentAfterTransaction({ number: 2, expectedDoc, - action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }), + action: () => tiptapEditor.commands.uploadAttachment({ file }), }); }); }); @@ -162,16 +196,16 @@ describe('content_editor/extensions/attachment', () => { await expectDocumentAfterTransaction({ number: 2, expectedDoc, - action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }), + action: () => tiptapEditor.commands.uploadAttachment({ file }), }); }); it('emits an alert event that includes an error message', (done) => { - tiptapEditor.commands.uploadAttachment({ file: imageFile }); + tiptapEditor.commands.uploadAttachment({ file }); eventHub.$on('alert', ({ message, variant }) => { expect(variant).toBe(VARIANT_DANGER); - expect(message).toBe('An error occurred while uploading the image. Please try again.'); + expect(message).toBe('An error occurred while uploading the file. Please try again.'); done(); }); }); diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 78cc1dcee01..96d8157dfde 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -467,6 +467,12 @@ RSpec.describe SearchHelper do describe '#show_user_search_tab?' do subject { show_user_search_tab? } + let(:current_user) { build(:user) } + + before do + allow(self).to receive(:current_user).and_return(current_user) + end + context 'when project search' do before do @project = :some_project @@ -481,11 +487,14 @@ RSpec.describe SearchHelper do end end - context 'when not project search' do + context 'when group search' do + before do + @group = :some_group + end + context 'when current_user can read_users_list' do before do - allow(self).to receive(:current_user).and_return(:the_current_user) - allow(self).to receive(:can?).with(:the_current_user, :read_users_list).and_return(true) + allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(true) end it { is_expected.to eq(true) } @@ -493,8 +502,33 @@ RSpec.describe SearchHelper do context 'when current_user cannot read_users_list' do before do - allow(self).to receive(:current_user).and_return(:the_current_user) - allow(self).to receive(:can?).with(:the_current_user, :read_users_list).and_return(false) + allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when global search' do + context 'when current_user can read_users_list' do + before do + allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(true) + end + + it { is_expected.to eq(true) } + + context 'when global_search_user_tab feature flag is disabled' do + before do + stub_feature_flags(global_search_users_tab: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when current_user cannot read_users_list' do + before do + allow(self).to receive(:can?).with(current_user, :read_users_list).and_return(false) end it { is_expected.to eq(false) } diff --git a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb index 15edbc22460..89b284feee0 100644 --- a/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb +++ b/spec/lib/gitlab/diff/rendered/notebook/diff_file_spec.rb @@ -63,6 +63,28 @@ RSpec.describe Gitlab::Diff::Rendered::Notebook::DiffFile do expect(nb_file.diff).to be_nil end end + + context 'timeout' do + it 'utilizes timeout for web' do + expect(Timeout).to receive(:timeout).with(described_class::RENDERED_TIMEOUT_FOREGROUND).and_call_original + + nb_file.diff + end + + it 'falls back to nil on timeout' do + allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception) + expect(Timeout).to receive(:timeout).and_raise(Timeout::Error) + + expect(nb_file.diff).to be_nil + end + + it 'utilizes longer timeout for sidekiq' do + allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true) + expect(Timeout).to receive(:timeout).with(described_class::RENDERED_TIMEOUT_BACKGROUND).and_call_original + + nb_file.diff + end + end end describe '#has_renderable?' do diff --git a/spec/models/alert_management/metric_image_spec.rb b/spec/models/alert_management/metric_image_spec.rb new file mode 100644 index 00000000000..dedbd6e501e --- /dev/null +++ b/spec/models/alert_management/metric_image_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AlertManagement::MetricImage do + subject { build(:alert_metric_image) } + + describe 'associations' do + it { is_expected.to belong_to(:alert) } + end + + describe 'validations' do + it { is_expected.to be_valid } + it { is_expected.to validate_presence_of(:file) } + it { is_expected.to validate_length_of(:url).is_at_most(255) } + it { is_expected.to validate_length_of(:url_text).is_at_most(128) } + end + + describe '.available_for?' do + subject { described_class.available_for?(issue.project) } + + let_it_be_with_refind(:issue) { create(:issue) } + + it { is_expected.to eq(true) } + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a60c6d4d5f3..fd87a388442 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1585,6 +1585,31 @@ RSpec.describe Ci::Build do it { is_expected.to eq('review/x') } end + + context 'when environment name uses a nested variable' do + let(:yaml_variables) do + [ + { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_NAME}' } + ] + end + + let(:build) do + create(:ci_build, + ref: 'master', + yaml_variables: yaml_variables, + environment: 'review/$ENVIRONMENT_NAME') + end + + it { is_expected.to eq('review/master') } + + context 'when the FF ci_expand_environment_name_and_url is disabled' do + before do + stub_feature_flags(ci_expand_environment_name_and_url: false) + end + + it { is_expected.to eq('review/${CI_COMMIT_REF_NAME}') } + end + end end describe '#expanded_kubernetes_namespace' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9f45487dbe4..5f2777b12d2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1560,7 +1560,7 @@ RSpec.describe User do end it 'adds the confirmed primary email to emails' do - expect(user.emails.confirmed.map(&:email)).not_to include(user.email) + expect(user.emails.confirmed.map(&:email)).not_to include(user.unconfirmed_email) user.confirm @@ -1619,14 +1619,23 @@ RSpec.describe User do context 'when the email is changed but not confirmed' do let(:user) { create(:user, email: 'primary@example.com') } - it 'does not add the new email to emails yet' do + before do user.update!(email: 'new_primary@example.com') + end + it 'does not add the new email to emails yet' do expect(user.unconfirmed_email).to eq('new_primary@example.com') expect(user.email).to eq('primary@example.com') expect(user).to be_confirmed expect(user.emails.pluck(:email)).not_to include('new_primary@example.com') end + + it 'adds the new email to emails upon confirmation' do + user.confirm + expect(user.email).to eq('new_primary@example.com') + expect(user).to be_confirmed + expect(user.emails.pluck(:email)).to include('new_primary@example.com') + end end context 'when the user is created as not confirmed' do @@ -1636,6 +1645,11 @@ RSpec.describe User do expect(user).not_to be_confirmed expect(user.emails.pluck(:email)).not_to include('primary@example.com') end + + it 'adds the email to emails upon confirmation' do + user.confirm + expect(user.emails.pluck(:email)).to include('primary@example.com') + end end context 'when the user is created as confirmed' do diff --git a/spec/policies/alert_management/alert_policy_spec.rb b/spec/policies/alert_management/alert_policy_spec.rb index 3e08d8b4ccc..2027c205c7b 100644 --- a/spec/policies/alert_management/alert_policy_spec.rb +++ b/spec/policies/alert_management/alert_policy_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' RSpec.describe AlertManagement::AlertPolicy, :models do - let(:alert) { create(:alert_management_alert) } - let(:project) { alert.project } - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:alert) { create(:alert_management_alert, project: project, issue: incident) } + let_it_be(:incident) { nil } subject(:policy) { described_class.new(user, alert) } @@ -21,5 +22,50 @@ RSpec.describe AlertManagement::AlertPolicy, :models do it { is_expected.to be_allowed :read_alert_management_alert } it { is_expected.to be_allowed :update_alert_management_alert } end + + shared_examples 'does not allow metric image reads' do + it { expect(policy).to be_disallowed(:read_alert_management_metric_image) } + end + + shared_examples 'does not allow metric image updates' do + specify do + expect(policy).to be_disallowed(:upload_alert_management_metric_image) + expect(policy).to be_disallowed(:destroy_alert_management_metric_image) + end + end + + shared_examples 'allows metric image reads' do + it { expect(policy).to be_allowed(:read_alert_management_metric_image) } + end + + shared_examples 'allows metric image updates' do + specify do + expect(policy).to be_allowed(:upload_alert_management_metric_image) + expect(policy).to be_allowed(:destroy_alert_management_metric_image) + end + end + + context 'when user is not a member' do + include_examples 'does not allow metric image reads' + include_examples 'does not allow metric image updates' + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + include_examples 'does not allow metric image reads' + include_examples 'does not allow metric image updates' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + include_examples 'allows metric image reads' + include_examples 'allows metric image updates' + end end end diff --git a/spec/requests/api/alert_management_alerts_spec.rb b/spec/requests/api/alert_management_alerts_spec.rb new file mode 100644 index 00000000000..99293e5ae95 --- /dev/null +++ b/spec/requests/api/alert_management_alerts_spec.rb @@ -0,0 +1,411 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::AlertManagementAlerts do + let_it_be(:creator) { create(:user) } + let_it_be(:project) do + create(:project, :public, creator_id: creator.id, namespace: creator.namespace) + end + + let_it_be(:user) { create(:user) } + let_it_be(:alert) { create(:alert_management_alert, project: project) } + + describe 'PUT /projects/:id/alert_management_alerts/:alert_iid/metric_images/authorize' do + include_context 'workhorse headers' + + before do + project.add_developer(user) + end + + subject do + post api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images/authorize", user), + headers: workhorse_headers + end + + it 'authorizes uploading with workhorse header' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + it 'rejects requests that bypassed gitlab-workhorse' do + workhorse_headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_uploads_object_storage(MetricImageUploader, enabled: true, direct_upload: true) + end + + it 'responds with status 200, location of file remote store and object details' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response).not_to have_key('TempPath') + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).to have_key('MultipartUpload') + end + end + + context 'when direct upload is disabled' do + before do + stub_uploads_object_storage(MetricImageUploader, enabled: true, direct_upload: false) + end + + it 'handles as a local file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(MetricImageUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + end + end + end + end + + describe 'POST /projects/:id/alert_management_alerts/:alert_iid/metric_images' do + include WorkhorseHelpers + using RSpec::Parameterized::TableSyntax + + include_context 'workhorse headers' + + let(:file) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:file_name) { 'rails_sample.jpg' } + let(:url) { 'http://gitlab.com' } + let(:url_text) { 'GitLab' } + + let(:params) { { url: url, url_text: url_text } } + + subject do + workhorse_finalize( + api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images", user), + method: :post, + file_key: :file, + params: params.merge(file: file), + headers: workhorse_headers, + send_rewritten_field: true + ) + end + + shared_examples 'can_upload_metric_image' do + it 'creates a new metric image' do + subject + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['filename']).to eq(file_name) + expect(json_response['url']).to eq(url) + expect(json_response['url_text']).to eq(url_text) + expect(json_response['created_at']).not_to be_nil + expect(json_response['id']).not_to be_nil + file_path_regex = %r{/uploads/-/system/alert_management_metric_image/file/\d+/#{file_name}} + expect(json_response['file_path']).to match(file_path_regex) + end + end + + shared_examples 'unauthorized_upload' do + it 'disallows the upload' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') + end + end + + where(:user_role, :expected_status) do + :guest | :unauthorized_upload + :reporter | :unauthorized_upload + :developer | :can_upload_metric_image + end + + with_them do + before do + # Local storage + stub_uploads_object_storage(MetricImageUploader, enabled: false) + allow_next_instance_of(MetricImageUploader) do |uploader| + allow(uploader).to receive(:file_storage?).and_return(true) + end + + project.send("add_#{user_role}", user) + end + + it_behaves_like "#{params[:expected_status]}" + end + + context 'file size too large' do + before do + allow_next_instance_of(UploadedFile) do |upload_file| + allow(upload_file).to receive(:size).and_return(AlertManagement::MetricImage::MAX_FILE_SIZE + 1) + end + end + + it 'returns an error' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to match(/File is too large/) + end + end + + context 'error when saving' do + before do + project.add_developer(user) + + allow_next_instance_of(::AlertManagement::MetricImages::UploadService) do |service| + error = instance_double(ServiceResponse, success?: false, message: 'some error', http_status: :bad_request) + allow(service).to receive(:execute).and_return(error) + end + end + + it 'returns an error' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to match(/some error/) + end + end + + context 'object storage enabled' do + before do + # Object storage + stub_uploads_object_storage(MetricImageUploader) + + allow_next_instance_of(MetricImageUploader) do |uploader| + allow(uploader).to receive(:file_storage?).and_return(true) + end + project.add_developer(user) + end + + it_behaves_like 'can_upload_metric_image' + + it 'uploads to remote storage' do + subject + + last_upload = AlertManagement::MetricImage.last.uploads.last + expect(last_upload.store).to eq(::ObjectStorage::Store::REMOTE) + end + end + end + + describe 'GET /projects/:id/alert_management_alerts/:alert_iid/metric_images' do + using RSpec::Parameterized::TableSyntax + + let!(:image) { create(:alert_metric_image, alert: alert) } + + subject { get api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images", user) } + + shared_examples 'can_read_metric_image' do + it 'can read the metric images' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first).to match( + { + id: image.id, + created_at: image.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + filename: image.filename, + file_path: image.file_path, + url: image.url, + url_text: nil + }.with_indifferent_access + ) + end + end + + shared_examples 'unauthorized_read' do + it 'cannot read the metric images' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + where(:user_role, :public_project, :expected_status) do + :not_member | false | :unauthorized_read + :not_member | true | :unauthorized_read + :guest | false | :unauthorized_read + :reporter | false | :unauthorized_read + :developer | false | :can_read_metric_image + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :not_member + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project + end + + it_behaves_like "#{params[:expected_status]}" + end + end + + describe 'PUT /projects/:id/alert_management_alerts/:alert_iid/metric_images/:metric_image_id' do + using RSpec::Parameterized::TableSyntax + + let!(:image) { create(:alert_metric_image, alert: alert) } + let(:params) { { url: 'http://test.example.com', url_text: 'Example website 123' } } + + subject do + put api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images/#{image.id}", user), + params: params + end + + shared_examples 'can_update_metric_image' do + it 'can update the metric images' do + subject + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['url']).to eq(params[:url]) + expect(json_response['url_text']).to eq(params[:url_text]) + end + end + + shared_examples 'unauthorized_update' do + it 'cannot update the metric image' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + expect(image.reload).to eq(image) + end + end + + where(:user_role, :public_project, :expected_status) do + :not_member | false | :unauthorized_update + :not_member | true | :unauthorized_update + :guest | false | :unauthorized_update + :reporter | false | :unauthorized_update + :developer | false | :can_update_metric_image + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :not_member + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project + end + + it_behaves_like "#{params[:expected_status]}" + end + + context 'when user has access' do + before do + project.add_developer(user) + end + + context 'and metric image not found' do + subject do + put api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images/#{non_existing_record_id}", user) # rubocop: disable Layout/LineLength + end + + it 'returns an error' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Metric image not found') + end + end + + context 'metric image cannot be updated' do + let(:params) { { url_text: 'something_long' * 100 } } + + it 'returns an error' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq('Metric image could not be updated') + end + end + end + end + + describe 'DELETE /projects/:id/alert_management_alerts/:alert_iid/metric_images/:metric_image_id' do + using RSpec::Parameterized::TableSyntax + + let!(:image) { create(:alert_metric_image, alert: alert) } + + subject do + delete api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images/#{image.id}", user) + end + + shared_examples 'can delete metric image successfully' do + it 'can delete the metric images' do + subject + + expect(response).to have_gitlab_http_status(:no_content) + expect { image.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + shared_examples 'unauthorized delete' do + it 'cannot delete the metric image' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + expect(image.reload).to eq(image) + end + end + + where(:user_role, :public_project, :expected_status) do + :not_member | false | 'unauthorized delete' + :not_member | true | 'unauthorized delete' + :guest | false | 'unauthorized delete' + :reporter | false | 'unauthorized delete' + :developer | false | 'can delete metric image successfully' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :not_member + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project + end + + it_behaves_like "#{params[:expected_status]}" + end + + context 'when user has access' do + before do + project.add_developer(user) + end + + context 'when metric image not found' do + subject do + delete api("/projects/#{project.id}/alert_management_alerts/#{alert.iid}/metric_images/#{non_existing_record_id}", user) # rubocop: disable Layout/LineLength + end + + it 'returns an error' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('Metric image not found') + end + end + + context 'when error when deleting' do + before do + allow_next_instance_of(AlertManagement::AlertsFinder) do |finder| + allow(finder).to receive(:execute).and_return([alert]) + end + + allow(alert).to receive_message_chain('metric_images.find_by_id') { image } + allow(image).to receive(:destroy).and_return(false) + end + + it 'returns an error' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq('Metric image could not be deleted') + end + end + end + end +end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index d8067321944..7e6d80c047c 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do it 'executes a limited number of queries' do control_count = ActiveRecord::QueryRecorder.new { subject }.count - expect(control_count).to be <= 105 + expect(control_count).to be <= 108 end it 'schedules an import using a namespace' do diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb index d1ddf8a6d6a..41646d1b515 100644 --- a/spec/routing/uploads_routing_spec.rb +++ b/spec/routing/uploads_routing_spec.rb @@ -21,6 +21,17 @@ RSpec.describe 'Uploads', 'routing' do ) end + it 'allows fetching alert metric metric images' do + expect(get('/uploads/-/system/alert_management_metric_image/file/1/test.jpg')).to route_to( + controller: 'uploads', + action: 'show', + model: 'alert_management_metric_image', + id: '1', + filename: 'test.jpg', + mounted_as: 'file' + ) + end + it 'does not allow creating uploads for other models' do unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w(personal_snippet user) diff --git a/spec/services/alert_management/metric_images/upload_service_spec.rb b/spec/services/alert_management/metric_images/upload_service_spec.rb new file mode 100644 index 00000000000..527d9db0fd9 --- /dev/null +++ b/spec/services/alert_management/metric_images/upload_service_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AlertManagement::MetricImages::UploadService do + subject(:service) { described_class.new(alert, current_user, params) } + + let_it_be_with_refind(:project) { create(:project) } + let_it_be_with_refind(:alert) { create(:alert_management_alert, project: project) } + let_it_be_with_refind(:current_user) { create(:user) } + + let(:params) do + { + file: fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg'), + url: 'https://www.gitlab.com' + } + end + + describe '#execute' do + subject { service.execute } + + shared_examples 'uploads the metric' do + it 'uploads the metric and returns a success' do + expect { subject }.to change(AlertManagement::MetricImage, :count).by(1) + expect(subject.success?).to eq(true) + expect(subject.payload).to match({ metric: instance_of(AlertManagement::MetricImage), alert: alert }) + end + end + + shared_examples 'no metric saved, an error given' do |message| + it 'returns an error and does not upload', :aggregate_failures do + expect(subject.success?).to eq(false) + expect(subject.message).to match(a_string_matching(message)) + expect(AlertManagement::MetricImage.count).to eq(0) + end + end + + context 'user does not have permissions' do + it_behaves_like 'no metric saved, an error given', 'You are not authorized to upload metric images' + end + + context 'user has permissions' do + before_all do + project.add_developer(current_user) + end + + it_behaves_like 'uploads the metric' + + context 'no url given' do + let(:params) do + { + file: fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') + } + end + + it_behaves_like 'uploads the metric' + end + + context 'record invalid' do + let(:params) do + { + file: fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain'), + url: 'https://www.gitlab.com' + } + end + + it_behaves_like 'no metric saved, an error given', /File does not have a supported extension. Only png, jpg, jpeg, gif, bmp, tiff, ico, and webp are supported/ # rubocop: disable Layout/LineLength + end + + context 'user is guest' do + before_all do + project.add_guest(current_user) + end + + it_behaves_like 'no metric saved, an error given', 'You are not authorized to upload metric images' + end + end + end +end diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index 6996563fdb8..0859aa2c9d1 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -286,6 +286,37 @@ RSpec.describe Deployments::UpdateEnvironmentService do end end + context 'when environment url uses a nested variable' do + let(:yaml_variables) do + [ + { key: 'MAIN_DOMAIN', value: '${STACK_NAME}.example.com' }, + { key: 'STACK_NAME', value: 'appname-${ENVIRONMENT_NAME}' }, + { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_SLUG}' } + ] + end + + let(:job) do + create(:ci_build, + :with_deployment, + pipeline: pipeline, + ref: 'master', + environment: 'production', + project: project, + yaml_variables: yaml_variables, + options: { environment: { name: 'production', url: 'http://$MAIN_DOMAIN' } }) + end + + it { is_expected.to eq('http://appname-master.example.com') } + + context 'when the FF ci_expand_environment_name_and_url is disabled' do + before do + stub_feature_flags(ci_expand_environment_name_and_url: false) + end + + it { is_expected.to eq('http://${STACK_NAME}.example.com') } + end + end + context 'when yaml environment does not have url' do let(:job) { create(:ci_build, :with_deployment, pipeline: pipeline, environment: 'staging', project: project) } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 7cf065536ea..c5c5af3cb01 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -135,10 +135,22 @@ RSpec.describe Projects::CreateService, '#execute' do create_project(user, opts) end - it 'builds associated project settings' do + it 'creates associated project settings' do project = create_project(user, opts) - expect(project.project_setting).to be_new_record + expect(project.project_setting).to be_persisted + end + + context 'create_project_settings feature flag is disabled' do + before do + stub_feature_flags(create_project_settings: false) + end + + it 'builds associated project settings' do + project = create_project(user, opts) + + expect(project.project_setting).to be_new_record + end end it_behaves_like 'storing arguments in the application context' do |