diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-05 15:09:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-05 15:09:12 +0000 |
commit | e2937892231e082f4981c31e25cb8d1cca36ea60 (patch) | |
tree | a543551ce5980395b9ee826c78e83d4d9c1ae9d4 /spec | |
parent | fdb953945da752dc52c1957f64a179de39f507e5 (diff) | |
download | gitlab-ce-e2937892231e082f4981c31e25cb8d1cca36ea60.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
28 files changed, 868 insertions, 109 deletions
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 55bdf4c244e..cbd1ae628d1 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -17,7 +17,6 @@ RSpec.describe 'Expand and collapse diffs', :js do # Ensure that undiffable.md is in .gitattributes project.repository.copy_gitattributes(branch) visit project_commit_path(project, project.commit(branch)) - execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') end def file_container(filename) @@ -191,10 +190,6 @@ RSpec.describe 'Expand and collapse diffs', :js do expect(small_diff).to have_selector('.code') expect(small_diff).not_to have_selector('.nothing-here-block') end - - it 'does not make a new HTTP request' do - expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md')) - end end end @@ -264,7 +259,6 @@ RSpec.describe 'Expand and collapse diffs', :js do find('.note-textarea') wait_for_requests - execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') end it 'reloads the page with all diffs expanded' do @@ -300,10 +294,6 @@ RSpec.describe 'Expand and collapse diffs', :js do expect(small_diff).to have_selector('.code') expect(small_diff).not_to have_selector('.nothing-here-block') end - - it 'does not make a new HTTP request' do - expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md')) - end end end end diff --git a/spec/features/projects/show/user_uploads_files_spec.rb b/spec/features/projects/show/user_uploads_files_spec.rb index b7c5d324d93..2030c4d998a 100644 --- a/spec/features/projects/show/user_uploads_files_spec.rb +++ b/spec/features/projects/show/user_uploads_files_spec.rb @@ -34,33 +34,23 @@ RSpec.describe 'Projects > Show > User uploads files' do include_examples 'it uploads and commit a new file to a forked project' end - context 'with an empty repo' do - let(:project) { create(:project, :empty_repo, creator: user) } - - context 'when in the empty_repo_upload experiment' do - before do - stub_experiments(empty_repo_upload: :candidate) - - visit(project_path(project)) - end - - it 'uploads and commits a new text file', :js do - click_link('Upload file') + context 'when in the empty_repo_upload experiment' do + before do + stub_experiments(empty_repo_upload: :candidate) - drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + visit(project_path(project)) + end - page.within('#modal-upload-blob') do - fill_in(:commit_message, with: 'New commit message') - end + context 'with an empty repo' do + let(:project) { create(:project, :empty_repo, creator: user) } - click_button('Upload file') + include_examples 'uploads and commits a new text file via "upload file" button' + end - wait_for_requests + context 'with a nonempty repo' do + let(:project) { create(:project, :repository, creator: user) } - expect(page).to have_content('New commit message') - expect(page).to have_content('Lorem ipsum dolor sit amet') - expect(page).to have_content('Sed ut perspiciatis unde omnis') - end + include_examples 'uploads and commits a new text file via "upload file" button' end end end diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap index ef6a7621b51..1f8429af7dd 100644 --- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap @@ -41,14 +41,14 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] role="presentation" > <a - aria-controls="__BVID__22" + aria-controls="__BVID__19" aria-disabled="true" aria-posinset="2" aria-selected="false" aria-setsize="3" class="nav-link disabled disabled gl-tab-nav-item" href="#" - id="__BVID__22___BV_tab_button__" + id="__BVID__19___BV_tab_button__" role="tab" tabindex="-1" target="_self" @@ -112,7 +112,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] for="integration-type" id="integration-type__BV_label_" > - Select integration type + 1.Select integration type </label> <div class="bv-no-focus-ring" @@ -150,19 +150,6 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] > <!----> - <span> - Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the - <a - class="gl-link gl-display-inline-block" - href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html" - rel="noopener noreferrer" - target="_blank" - > - GitLab documentation - </a> - to learn more about configuring your endpoint. - </span> - <label class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal" > @@ -206,10 +193,6 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] <!----> <!----> - - <!----> - - <!----> </div> <div @@ -264,12 +247,25 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] > <div aria-hidden="true" - aria-labelledby="__BVID__22___BV_tab_button__" + aria-labelledby="__BVID__19___BV_tab_button__" class="tab-pane disabled" - id="__BVID__22" + id="__BVID__19" role="tabpanel" style="display: none;" > + <span> + Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the + <a + class="gl-link gl-display-inline-block" + href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html" + rel="noopener noreferrer" + target="_blank" + > + GitLab documentation + </a> + to learn more about configuring your endpoint. + </span> + <fieldset class="form-group gl-form-group" id="integration-webhook" diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js index f8e192dfae9..68a39b73eff 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -43,10 +43,10 @@ describe('AlertsSettingsForm', () => { }); }; - const findForm = () => wrapper.find(GlForm); - const findSelect = () => wrapper.find(GlFormSelect); - const findFormFields = () => wrapper.findAll(GlFormInput); - const findFormToggle = () => wrapper.find(GlToggle); + const findForm = () => wrapper.findComponent(GlForm); + const findSelect = () => wrapper.findComponent(GlFormSelect); + const findFormFields = () => wrapper.findAllComponents(GlFormInput); + const findFormToggle = () => wrapper.findComponent(GlToggle); const findSamplePayloadSection = () => wrapper.find('[data-testid="sample-payload-section"]'); const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`); const findMappingBuilder = () => wrapper.findComponent(MappingBuilder); @@ -56,7 +56,7 @@ describe('AlertsSettingsForm', () => { const findJsonTestSubmit = () => wrapper.find(`[data-testid="send-test-alert"]`); const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`); const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`); - const findTabs = () => wrapper.findAll(GlTab); + const findTabs = () => wrapper.findAllComponents(GlTab); afterEach(() => { if (wrapper) { diff --git a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap b/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap new file mode 100644 index 00000000000..5f05b7fc68b --- /dev/null +++ b/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Ref selector component footer slot passes the expected slot props 1`] = ` +Object { + "isLoading": false, + "matches": Object { + "branches": Object { + "error": null, + "list": Array [ + Object { + "default": false, + "name": "add_images_and_changes", + }, + Object { + "default": false, + "name": "conflict-contains-conflict-markers", + }, + Object { + "default": false, + "name": "deleted-image-test", + }, + Object { + "default": false, + "name": "diff-files-image-to-symlink", + }, + Object { + "default": false, + "name": "diff-files-symlink-to-image", + }, + Object { + "default": false, + "name": "markdown", + }, + Object { + "default": true, + "name": "master", + }, + ], + "totalCount": 123, + }, + "commits": Object { + "error": null, + "list": Array [ + Object { + "name": "b83d6e39", + "subtitle": "Merge branch 'branch-merged' into 'master'", + "value": "b83d6e391c22777fca1ed3012fce84f633d7fed0", + }, + ], + "totalCount": 1, + }, + "tags": Object { + "error": null, + "list": Array [ + Object { + "name": "v1.1.1", + }, + Object { + "name": "v1.1.0", + }, + Object { + "name": "v1.0.0", + }, + ], + "totalCount": 456, + }, + }, + "query": "abcd1234", +} +`; diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index cce365b1949..715c862cf39 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -1,7 +1,8 @@ -import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { merge, last } from 'lodash'; import Vuex from 'vuex'; import { trimText } from 'helpers/text_helper'; import { ENTER_KEY } from '~/lib/utils/keys'; @@ -34,26 +35,30 @@ describe('Ref selector component', () => { let commitApiCallSpy; let requestSpies; - const createComponent = (props = {}, attrs = {}) => { - wrapper = mount(RefSelector, { - propsData: { - projectId, - value: '', - ...props, - }, - attrs, - listeners: { - // simulate a parent component v-model binding - input: (selectedRef) => { - wrapper.setProps({ value: selectedRef }); + const createComponent = (mountOverrides = {}) => { + wrapper = mount( + RefSelector, + merge( + { + propsData: { + projectId, + value: '', + }, + listeners: { + // simulate a parent component v-model binding + input: (selectedRef) => { + wrapper.setProps({ value: selectedRef }); + }, + }, + stubs: { + GlSearchBoxByType: true, + }, + localVue, + store: createStore(), }, - }, - stubs: { - GlSearchBoxByType: true, - }, - localVue, - store: createStore(), - }); + mountOverrides, + ), + ); }; beforeEach(() => { @@ -183,7 +188,7 @@ describe('Ref selector component', () => { const id = 'git-ref'; beforeEach(() => { - createComponent({}, { id }); + createComponent({ attrs: { id } }); return waitForRequests(); }); @@ -197,7 +202,7 @@ describe('Ref selector component', () => { const preselectedRef = fixtures.branches[0].name; beforeEach(() => { - createComponent({ value: preselectedRef }); + createComponent({ propsData: { value: preselectedRef } }); return waitForRequests(); }); @@ -611,7 +616,7 @@ describe('Ref selector component', () => { `( 'only calls $reqsCalled requests when $enabledRefTypes are enabled', async ({ enabledRefTypes, reqsCalled, reqsNotCalled }) => { - createComponent({ enabledRefTypes }); + createComponent({ propsData: { enabledRefTypes } }); await waitForRequests(); @@ -621,7 +626,7 @@ describe('Ref selector component', () => { ); it('only calls commitApiCallSpy when REF_TYPE_COMMITS is enabled', async () => { - createComponent({ enabledRefTypes: [REF_TYPE_COMMITS] }); + createComponent({ propsData: { enabledRefTypes: [REF_TYPE_COMMITS] } }); updateQuery('abcd1234'); await waitForRequests(); @@ -632,7 +637,7 @@ describe('Ref selector component', () => { }); it('triggers another search if enabled ref types change', async () => { - createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES] }); + createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES] } }); await waitForRequests(); expect(branchesApiCallSpy).toHaveBeenCalledTimes(1); @@ -648,7 +653,7 @@ describe('Ref selector component', () => { }); it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => { - createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] }); + createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] } }); updateQuery('abcd1234'); await waitForRequests(); @@ -670,7 +675,7 @@ describe('Ref selector component', () => { `( 'hides section headers if a single ref type is enabled', async ({ enabledRefType, findVisibleSection, findHiddenSections }) => { - createComponent({ enabledRefTypes: [enabledRefType] }); + createComponent({ propsData: { enabledRefTypes: [enabledRefType] } }); updateQuery('abcd1234'); await waitForRequests(); @@ -682,4 +687,70 @@ describe('Ref selector component', () => { }, ); }); + + describe('validation state', () => { + const invalidClass = 'gl-inset-border-1-red-500!'; + const isInvalidClassApplied = () => wrapper.find(GlDropdown).props('toggleClass')[invalidClass]; + + describe('valid state', () => { + describe('when the state prop is not provided', () => { + it('does not render a red border', () => { + createComponent(); + + expect(isInvalidClassApplied()).toBe(false); + }); + }); + + describe('when the state prop is true', () => { + it('does not render a red border', () => { + createComponent({ propsData: { state: true } }); + + expect(isInvalidClassApplied()).toBe(false); + }); + }); + }); + + describe('invalid state', () => { + it('renders the dropdown with a red border if the state prop is false', () => { + createComponent({ propsData: { state: false } }); + + expect(isInvalidClassApplied()).toBe(true); + }); + }); + }); + + describe('footer slot', () => { + const footerContent = 'This is the footer content'; + const createFooter = jest.fn().mockImplementation(function createMockFooter() { + return this.$createElement('div', { attrs: { 'data-testid': 'footer-content' } }, [ + footerContent, + ]); + }); + + beforeEach(() => { + createComponent({ + scopedSlots: { footer: createFooter }, + }); + + updateQuery('abcd1234'); + + return waitForRequests(); + }); + + afterEach(() => { + createFooter.mockClear(); + }); + + it('allows custom content to be shown at the bottom of the dropdown using the footer slot', () => { + expect(wrapper.find(`[data-testid="footer-content"]`).text()).toBe(footerContent); + }); + + it('passes the expected slot props', () => { + // The createFooter function gets called every time one of the scoped properties + // is updated. For the sake of this test, we'll just test the last call, which + // represents the final state of the slot props. + const lastCallProps = last(createFooter.mock.calls)[0]; + expect(lastCallProps).toMatchSnapshot(); + }); + }); }); diff --git a/spec/frontend/single_file_diff_spec.js b/spec/frontend/single_file_diff_spec.js new file mode 100644 index 00000000000..8718152655f --- /dev/null +++ b/spec/frontend/single_file_diff_spec.js @@ -0,0 +1,96 @@ +import MockAdapter from 'axios-mock-adapter'; +import $ from 'jquery'; +import { setHTMLFixture } from 'helpers/fixtures'; +import axios from '~/lib/utils/axios_utils'; +import SingleFileDiff from '~/single_file_diff'; + +describe('SingleFileDiff', () => { + let mock = new MockAdapter(axios); + const blobDiffPath = '/mock-path'; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(blobDiffPath).replyOnce(200, { html: `<div class="diff-content">MOCKED</div>` }); + }); + + afterEach(() => { + mock.restore(); + }); + + it('loads diff via axios exactly once for collapsed diffs', async () => { + setHTMLFixture(` + <div class="diff-file"> + <div class="js-file-title"> + MOCK TITLE + </div> + + <div class="diff-content"> + <div class="diff-viewer" data-type="simple"> + <div + class="nothing-here-block diff-collapsed" + data-diff-for-path="${blobDiffPath}" + > + MOCK CONTENT + </div> + </div> + </div> + </div> +`); + + // Collapsed is the default state + const diff = new SingleFileDiff(document.querySelector('.diff-file')); + expect(diff.isOpen).toBe(false); + expect(diff.content).toBeNull(); + expect(diff.diffForPath).toEqual(blobDiffPath); + + // Opening for the first time + await diff.toggleDiff($(document.querySelector('.js-file-title'))); + expect(diff.isOpen).toBe(true); + expect(diff.content).not.toBeNull(); + + // Collapsing again + await diff.toggleDiff($(document.querySelector('.js-file-title'))); + expect(diff.isOpen).toBe(false); + expect(diff.content).not.toBeNull(); + + mock.onGet(blobDiffPath).replyOnce(400, ''); + + // Opening again + await diff.toggleDiff($(document.querySelector('.js-file-title'))); + expect(diff.isOpen).toBe(true); + expect(diff.content).not.toBeNull(); + + expect(mock.history.get.length).toBe(1); + }); + + it('does not load diffs via axios for already expanded diffs', async () => { + setHTMLFixture(` + <div class="diff-file"> + <div class="js-file-title"> + MOCK TITLE + </div> + + <div class="diff-content"> + EXPANDED MOCK CONTENT + </div> + </div> +`); + + // Opened is the default state + const diff = new SingleFileDiff(document.querySelector('.diff-file')); + expect(diff.isOpen).toBe(true); + expect(diff.content).not.toBeNull(); + expect(diff.diffForPath).toEqual(undefined); + + // Collapsing for the first time + await diff.toggleDiff($(document.querySelector('.js-file-title'))); + expect(diff.isOpen).toBe(false); + expect(diff.content).not.toBeNull(); + + // Opening again + await diff.toggleDiff($(document.querySelector('.js-file-title'))); + expect(diff.isOpen).toBe(true); + + expect(mock.history.get.length).toBe(0); + }); +}); diff --git a/spec/graphql/mutations/user_callouts/create_spec.rb b/spec/graphql/mutations/user_callouts/create_spec.rb new file mode 100644 index 00000000000..93f227d8b82 --- /dev/null +++ b/spec/graphql/mutations/user_callouts/create_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::UserCallouts::Create do + let(:current_user) { create(:user) } + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + + describe '#resolve' do + subject(:resolve) { mutation.resolve(feature_name: feature_name) } + + context 'when feature name is not supported' do + let(:feature_name) { 'not_supported' } + + it 'does not create a user callout' do + expect { resolve }.not_to change(UserCallout, :count).from(0) + end + + it 'returns error about feature name not being supported' do + expect(resolve[:errors]).to include("Feature name is not included in the list") + end + end + + context 'when feature name is supported' do + let(:feature_name) { UserCallout.feature_names.each_key.first.to_s } + + it 'creates a user callout' do + expect { resolve }.to change(UserCallout, :count).from(0).to(1) + end + + it 'sets dismissed_at for the user callout' do + freeze_time do + expect(resolve[:user_callout].dismissed_at).to eq(Time.current) + end + end + + it 'has no errors' do + expect(resolve[:errors]).to be_empty + end + end + end +end diff --git a/spec/graphql/types/user_callout_feature_name_enum_spec.rb b/spec/graphql/types/user_callout_feature_name_enum_spec.rb new file mode 100644 index 00000000000..28755e1301b --- /dev/null +++ b/spec/graphql/types/user_callout_feature_name_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['UserCalloutFeatureNameEnum'] do + specify { expect(described_class.graphql_name).to eq('UserCalloutFeatureNameEnum') } + + it 'exposes all the existing user callout feature names' do + expect(described_class.values.keys).to match_array(::UserCallout.feature_names.keys.map(&:upcase)) + end +end diff --git a/spec/graphql/types/user_callout_type_spec.rb b/spec/graphql/types/user_callout_type_spec.rb new file mode 100644 index 00000000000..b26b85a4e8b --- /dev/null +++ b/spec/graphql/types/user_callout_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['UserCallout'] do + specify { expect(described_class.graphql_name).to eq('UserCallout') } + + it 'has expected fields' do + expect(described_class).to have_graphql_fields(:feature_name, :dismissed_at) + end +end diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index 5b3662383d8..d9e67ff348b 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -31,6 +31,7 @@ RSpec.describe GitlabSchema.types['User'] do groupCount projectMemberships starredProjects + callouts ] expect(described_class).to have_graphql_fields(*expected_fields) @@ -44,4 +45,12 @@ RSpec.describe GitlabSchema.types['User'] do is_expected.to have_graphql_resolver(Resolvers::Users::SnippetsResolver) end end + + describe 'callouts field' do + subject { described_class.fields['callouts'] } + + it 'returns user callouts' do + is_expected.to have_graphql_type(Types::UserCalloutType.connection_type) + end + end end diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb new file mode 100644 index 00000000000..db30446fa95 --- /dev/null +++ b/spec/helpers/ide_helper_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IdeHelper do + describe '#ide_data' do + let_it_be(:project) { create(:project) } + + before do + allow(helper).to receive(:current_user).and_return(project.creator) + end + + context 'when instance vars are not set' do + it 'returns instance data in the hash as nil' do + expect(helper.ide_data) + .to include( + 'branch-name' => nil, + 'file-path' => nil, + 'merge-request' => nil, + 'forked-project' => nil, + 'project' => nil + ) + end + end + + context 'when instance vars are set' do + it 'returns instance data in the hash' do + self.instance_variable_set(:@branch, 'master') + self.instance_variable_set(:@path, 'foo/bar') + self.instance_variable_set(:@merge_request, '1') + self.instance_variable_set(:@forked_project, project) + self.instance_variable_set(:@project, project) + + serialized_project = API::Entities::Project.represent(project).to_json + + expect(helper.ide_data) + .to include( + 'branch-name' => 'master', + 'file-path' => 'foo/bar', + 'merge-request' => '1', + 'forked-project' => serialized_project, + 'project' => serialized_project + ) + end + end + end +end diff --git a/spec/lib/gitlab/usage/docs/renderer_spec.rb b/spec/lib/gitlab/usage/docs/renderer_spec.rb index 07f25cfcfa7..f3b83a4a4b3 100644 --- a/spec/lib/gitlab/usage/docs/renderer_spec.rb +++ b/spec/lib/gitlab/usage/docs/renderer_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' +CODE_REGEX = %r{<code>(.*)</code>}.freeze + RSpec.describe Gitlab::Usage::Docs::Renderer do describe 'contents' do let(:dictionary_path) { Gitlab::Usage::Docs::Renderer::DICTIONARY_PATH } @@ -9,12 +11,12 @@ RSpec.describe Gitlab::Usage::Docs::Renderer do it 'generates dictionary for given items' do generated_dictionary = described_class.new(items).contents + generated_dictionary_keys = RDoc::Markdown .parse(generated_dictionary) .table_of_contents - .select { |metric_doc| metric_doc.level == 2 && !metric_doc.text.start_with?('info:') } - .map(&:text) - .map { |text| text.sub('<code>', '').sub('</code>', '') } + .select { |metric_doc| metric_doc.level == 3 } + .map { |item| item.text.match(CODE_REGEX)&.captures&.first } expect(generated_dictionary_keys).to match_array(items.keys) end diff --git a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb index 7002c76a7cf..7e7107ef1a6 100644 --- a/spec/lib/gitlab/usage/docs/value_formatter_spec.rb +++ b/spec/lib/gitlab/usage/docs/value_formatter_spec.rb @@ -10,11 +10,11 @@ RSpec.describe Gitlab::Usage::Docs::ValueFormatter do :data_source | 'redis' | 'Redis' :data_source | 'ruby' | 'Ruby' :introduced_by_url | 'http://test.com' | '[Introduced by](http://test.com)' - :tier | %w(gold premium) | 'gold, premium' - :distribution | %w(ce ee) | 'ce, ee' + :tier | %w(gold premium) | '`gold`, `premium`' + :distribution | %w(ce ee) | '`ce`, `ee`' :key_path | 'key.path' | '**`key.path`**' :milestone | '13.4' | '13.4' - :status | 'data_available' | 'data_available' + :status | 'data_available' | '`data_available`' end with_them do diff --git a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb index 717c3b8eca4..d0c3456aa26 100644 --- a/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb @@ -21,6 +21,14 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl end end + shared_examples_for 'not tracked merge request unique event' do + specify do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + subject + end + end + describe '.track_mr_diffs_action' do subject { described_class.track_mr_diffs_action(merge_request: merge_request) } @@ -332,4 +340,34 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl let(:action) { described_class::MR_REVIEWERS_CHANGED_ACTION } end end + + describe '.track_mr_including_ci_config' do + subject { described_class.track_mr_including_ci_config(user: user, merge_request: merge_request) } + + context 'when merge request includes a ci config change' do + before do + allow(merge_request).to receive(:diff_stats).and_return([double(path: 'abc.txt'), double(path: '.gitlab-ci.yml')]) + end + + it_behaves_like 'a tracked merge request unique event' do + let(:action) { described_class::MR_INCLUDING_CI_CONFIG_ACTION } + end + + context 'when FF usage_data_o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile is disabled' do + before do + stub_feature_flags(usage_data_o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile: false) + end + + it_behaves_like 'not tracked merge request unique event' + end + end + + context 'when merge request does not include any ci config change' do + before do + allow(merge_request).to receive(:diff_stats).and_return([double(path: 'abc.txt'), double(path: 'abc.xyz')]) + end + + it_behaves_like 'not tracked merge request unique event' + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 1aa2cbb5485..7b7046b1d77 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1353,7 +1353,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } let(:ineligible_total_categories) do - %w[source_code ci_secrets_management incident_management_alerts snippets terraform pipeline_authoring] + %w[source_code ci_secrets_management incident_management_alerts snippets terraform] end it 'has all known_events' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f6955ded748..31f4310fd63 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -581,7 +581,7 @@ RSpec.describe Ci::Build do end it 'that cannot handle build' do - expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) + expect_any_instance_of(Ci::Runner).to receive(:matches_build?).with(build).and_return(false) is_expected.to be_falsey end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 4c8212bf774..ff3551d2a18 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -350,6 +350,8 @@ RSpec.describe Ci::Runner do end describe '#can_pick?' do + using RSpec::Parameterized::TableSyntax + let_it_be(:pipeline) { create(:ci_pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:runner_project) { build.project } @@ -363,6 +365,11 @@ RSpec.describe Ci::Runner do let(:other_project) { create(:project) } let(:other_runner) { create(:ci_runner, :project, projects: [other_project], tag_list: tag_list, run_untagged: run_untagged) } + before do + # `can_pick?` is not used outside the runners available for the project + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + end + it 'cannot handle builds' do expect(other_runner.can_pick?(build)).to be_falsey end @@ -430,9 +437,32 @@ RSpec.describe Ci::Runner do expect(runner.can_pick?(build)).to be_truthy end end + + it 'does not query for owned or instance runners' do + expect(described_class).not_to receive(:owned_or_instance_wide) + + runner.can_pick?(build) + end + + context 'when feature flag ci_runners_short_circuit_assignable_for is disabled' do + before do + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + end + + it 'does not query for owned or instance runners' do + expect(described_class).to receive(:owned_or_instance_wide).and_call_original + + runner.can_pick?(build) + end + end end context 'when runner is not shared' do + before do + # `can_pick?` is not used outside the runners available for the project + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + end + context 'when runner is assigned to a project' do it 'can handle builds' do expect(runner.can_pick?(build)).to be_truthy @@ -500,6 +530,29 @@ RSpec.describe Ci::Runner do it { is_expected.to be_falsey } end end + + context 'matches tags' do + where(:run_untagged, :runner_tags, :build_tags, :result) do + true | [] | [] | true + true | [] | ['a'] | false + true | %w[a b] | ['a'] | true + true | ['a'] | %w[a b] | false + true | ['a'] | ['a'] | true + false | ['a'] | ['a'] | true + false | ['b'] | ['a'] | false + false | %w[a b] | ['a'] | true + end + + with_them do + let(:tag_list) { runner_tags } + + before do + build.tag_list = build_tags + end + + it { is_expected.to eq(result) } + end + end end describe '#status' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 551da6d0471..1ff6c7c5b53 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -4856,4 +4856,33 @@ RSpec.describe MergeRequest, factory_default: :keep do end end end + + describe '#includes_ci_config?' do + let(:merge_request) { build(:merge_request) } + let(:project) { merge_request.project } + + subject(:result) { merge_request.includes_ci_config? } + + before do + allow(merge_request).to receive(:diff_stats).and_return(diff_stats) + end + + context 'when diff_stats is nil' do + let(:diff_stats) {} + + it { is_expected.to eq(false) } + end + + context 'when diff_stats does not include the ci config path of the project' do + let(:diff_stats) { [double(path: 'abc.txt')] } + + it { is_expected.to eq(false) } + end + + context 'when diff_stats includes the ci config path of the project' do + let(:diff_stats) { [double(path: '.gitlab-ci.yml')] } + + it { is_expected.to eq(true) } + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6d0f01d2c8c..b4010ba6e31 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1599,7 +1599,7 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#any_runners?' do + describe '#any_active_runners?' do context 'shared runners' do let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) } let(:specific_runner) { create(:ci_runner, :project, projects: [project]) } @@ -1609,31 +1609,31 @@ RSpec.describe Project, factory_default: :keep do let(:shared_runners_enabled) { false } it 'has no runners available' do - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end it 'has a specific runner' do specific_runner - expect(project.any_runners?).to be_truthy + expect(project.any_active_runners?).to be_truthy end it 'has a shared runner, but they are prohibited to use' do shared_runner - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end it 'checks the presence of specific runner' do specific_runner - expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy + expect(project.any_active_runners? { |runner| runner == specific_runner }).to be_truthy end it 'returns false if match cannot be found' do specific_runner - expect(project.any_runners? { false }).to be_falsey + expect(project.any_active_runners? { false }).to be_falsey end end @@ -1643,19 +1643,19 @@ RSpec.describe Project, factory_default: :keep do it 'has a shared runner' do shared_runner - expect(project.any_runners?).to be_truthy + expect(project.any_active_runners?).to be_truthy end it 'checks the presence of shared runner' do shared_runner - expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy + expect(project.any_active_runners? { |runner| runner == shared_runner }).to be_truthy end it 'returns false if match cannot be found' do shared_runner - expect(project.any_runners? { false }).to be_falsey + expect(project.any_active_runners? { false }).to be_falsey end end end @@ -1669,13 +1669,13 @@ RSpec.describe Project, factory_default: :keep do let(:group_runners_enabled) { false } it 'has no runners available' do - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end it 'has a group runner, but they are prohibited to use' do group_runner - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end end @@ -1685,19 +1685,19 @@ RSpec.describe Project, factory_default: :keep do it 'has a group runner' do group_runner - expect(project.any_runners?).to be_truthy + expect(project.any_active_runners?).to be_truthy end it 'checks the presence of group runner' do group_runner - expect(project.any_runners? { |runner| runner == group_runner }).to be_truthy + expect(project.any_active_runners? { |runner| runner == group_runner }).to be_truthy end it 'returns false if match cannot be found' do group_runner - expect(project.any_runners? { false }).to be_falsey + expect(project.any_active_runners? { false }).to be_falsey end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c1e1d878e95..94e83d224e6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5482,4 +5482,43 @@ RSpec.describe User do end end end + + describe '#find_or_initialize_callout' do + subject(:find_or_initialize_callout) { user.find_or_initialize_callout(feature_name) } + + let(:user) { create(:user) } + let(:feature_name) { UserCallout.feature_names.each_key.first } + + context 'when callout exists' do + let!(:callout) { create(:user_callout, user: user, feature_name: feature_name) } + + it 'returns existing callout' do + expect(find_or_initialize_callout).to eq(callout) + end + end + + context 'when callout does not exist' do + context 'when feature name is valid' do + it 'initializes a new callout' do + expect(find_or_initialize_callout).to be_a_new(UserCallout) + end + + it 'is valid' do + expect(find_or_initialize_callout).to be_valid + end + end + + context 'when feature name is not valid' do + let(:feature_name) { 'notvalid' } + + it 'initializes a new callout' do + expect(find_or_initialize_callout).to be_a_new(UserCallout) + end + + it 'is not valid' do + expect(find_or_initialize_callout).not_to be_valid + end + end + end + end end diff --git a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb new file mode 100644 index 00000000000..cb67a60ebe4 --- /dev/null +++ b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a user callout' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let(:feature_name) { ::UserCallout.feature_names.each_key.first } + + let(:input) do + { + 'featureName' => feature_name + } + end + + let(:mutation) { graphql_mutation(:userCalloutCreate, input) } + let(:mutation_response) { graphql_mutation_response(:userCalloutCreate) } + + it 'creates user callout' do + freeze_time do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['userCallout']['featureName']).to eq(feature_name.upcase) + expect(mutation_response['userCallout']['dismissedAt']).to eq(Time.current.iso8601) + end + end +end diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index 805c1f1d82b..4f127e07b6b 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -3,7 +3,11 @@ require 'spec_helper' RSpec.describe IdeController do - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:creator) { project.creator } + let_it_be(:other_user) { create(:user) } + + let(:user) { creator } before do sign_in(user) @@ -14,4 +18,172 @@ RSpec.describe IdeController do get ide_url end + + describe '#index', :aggregate_failures do + subject { get route } + + shared_examples 'user cannot push code' do + include ProjectForksHelper + + let(:user) { other_user } + + context 'when user does not have fork' do + it 'does not instantiate forked_project instance var and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:forked_project)).to be_nil + end + end + + context 'when user has have fork' do + let!(:fork) { fork_project(project, user, repository: true) } + + it 'instantiates forked_project instance var and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:forked_project)).to eq fork + end + end + end + + context '/-/ide' do + let(:route) { '/-/ide' } + + it 'does not instantiate any instance var and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to be_nil + expect(assigns(:branch)).to be_nil + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + end + + context '/-/ide/project' do + let(:route) { '/-/ide/project' } + + it 'does not instantiate any instance var and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to be_nil + expect(assigns(:branch)).to be_nil + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + end + + context '/-/ide/project/:project' do + let(:route) { "/-/ide/project/#{project.full_path}" } + + it 'instantiates project instance var and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:branch)).to be_nil + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + + it_behaves_like 'user cannot push code' + + %w(edit blob tree).each do |action| + context "/-/ide/project/:project/#{action}" do + let(:route) { "/-/ide/project/#{project.full_path}/#{action}" } + + it 'instantiates project instance var and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:branch)).to be_nil + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + + it_behaves_like 'user cannot push code' + + context "/-/ide/project/:project/#{action}/:branch" do + let(:route) { "/-/ide/project/#{project.full_path}/#{action}/master" } + + it 'instantiates project and branch instance vars and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:branch)).to eq 'master' + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + + it_behaves_like 'user cannot push code' + + context "/-/ide/project/:project/#{action}/:branch/-" do + let(:route) { "/-/ide/project/#{project.full_path}/#{action}/branch/slash/-" } + + it 'instantiates project and branch instance vars and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:branch)).to eq 'branch/slash' + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + + it_behaves_like 'user cannot push code' + + context "/-/ide/project/:project/#{action}/:branch/-/:path" do + let(:route) { "/-/ide/project/#{project.full_path}/#{action}/master/-/foo/.bar" } + + it 'instantiates project, branch, and path instance vars and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:branch)).to eq 'master' + expect(assigns(:path)).to eq 'foo/.bar' + expect(assigns(:merge_request)).to be_nil + expect(assigns(:forked_project)).to be_nil + end + + it_behaves_like 'user cannot push code' + end + end + end + end + end + + context '/-/ide/project/:project/merge_requests/:merge_request_id' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" } + + it 'instantiates project and merge_request instance vars and return 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:project)).to eq project + expect(assigns(:branch)).to be_nil + expect(assigns(:path)).to be_nil + expect(assigns(:merge_request)).to eq merge_request.id.to_s + expect(assigns(:forked_project)).to be_nil + end + + it_behaves_like 'user cannot push code' + end + end + end end diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index 6cac49febda..2d9f80a249d 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Ci::UpdateBuildQueueService do context 'when feature flag ci_reduce_queries_when_ticking_runner_queue is disabled' do before do stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: false) + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) end it 'runs redundant queries using `owned_or_instance_wide` scope' do diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb index f21feb70bc5..65dd4fde676 100644 --- a/spec/services/merge_requests/after_create_service_spec.rb +++ b/spec/services/merge_requests/after_create_service_spec.rb @@ -32,6 +32,10 @@ RSpec.describe MergeRequests::AfterCreateService do .to receive(:track_create_mr_action) .with(user: merge_request.author) + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_mr_including_ci_config) + .with(user: merge_request.author, merge_request: merge_request) + execute_service end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index eeb782b573e..2abe7a23bfe 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -170,6 +170,18 @@ RSpec.describe MergeRequests::RefreshService do .not_to change { @merge_request.reload.merge_request_diff } end end + + it 'calls the merge request activity counter' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_mr_including_ci_config) + .with(user: @merge_request.author, merge_request: @merge_request) + + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_mr_including_ci_config) + .with(user: @another_merge_request.author, merge_request: @another_merge_request) + + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + end end context 'when pipeline exists for the source branch' do diff --git a/spec/services/users/dismiss_user_callout_service_spec.rb b/spec/services/users/dismiss_user_callout_service_spec.rb new file mode 100644 index 00000000000..22f84a939f7 --- /dev/null +++ b/spec/services/users/dismiss_user_callout_service_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::DismissUserCalloutService do + let(:user) { create(:user) } + + let(:service) do + described_class.new( + container: nil, current_user: user, params: { feature_name: UserCallout.feature_names.each_key.first } + ) + end + + describe '#execute' do + subject(:execute) { service.execute } + + it 'returns a user callout' do + expect(execute).to be_an_instance_of(UserCallout) + end + + it 'sets the dismisse_at attribute to current time' do + freeze_time do + expect(execute).to have_attributes(dismissed_at: Time.current) + end + end + end +end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 4411c91d479..7daebcbb0fe 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -94,3 +94,23 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do expect(page).to have_content('Sed ut perspiciatis unde omnis') end end + +RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do + it 'uploads and commits a new text file via "upload file" button', :js do + find('.js-upload-file-experiment-trigger', text: 'Upload file').click + + drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) + + page.within('#modal-upload-blob') do + fill_in(:commit_message, with: 'New commit message') + end + + click_button('Upload file') + + wait_for_requests + + expect(page).to have_content('New commit message') + expect(page).to have_content('Lorem ipsum dolor sit amet') + expect(page).to have_content('Sed ut perspiciatis unde omnis') + end +end |