diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-18 21:10:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-18 21:10:13 +0300 |
commit | dd6e32bf47776514b8fe2abcfe7998503f16baab (patch) | |
tree | f47d5c93958146fcfc6dd0dba3a6e11a2f8061ab /spec | |
parent | c8cc2fe990c52cabcb9912b2b01b5bf16b33d88f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
42 files changed, 358 insertions, 3409 deletions
diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb index cedda8d0854..cd9c8a8bfbb 100644 --- a/spec/factories/packages.rb +++ b/spec/factories/packages.rb @@ -162,6 +162,12 @@ FactoryBot.define do pkg.nuget_metadatum = build(:nuget_metadatum) end end + + trait(:with_symbol_package) do + after :create do |package| + create :package_file, :snupkg, package: package, file_name: "#{package.name}.#{package.version}.snupkg" + end + end end factory :pypi_package do diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb index d82fbe02311..9b60f769505 100644 --- a/spec/factories/packages/package_file.rb +++ b/spec/factories/packages/package_file.rb @@ -271,6 +271,14 @@ FactoryBot.define do size { 300.kilobytes } end + trait(:snupkg) do + package + file_fixture { 'spec/fixtures/packages/nuget/package.snupkg' } + file_name { 'package.snupkg' } + file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' } + size { 300.kilobytes } + end + trait(:gem) do package file_fixture { 'spec/fixtures/packages/rubygems/package-0.0.1.gem' } diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index 5b11d9cb919..a5094be2bcf 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -259,8 +259,8 @@ RSpec.describe 'Merge request > Batch comments', :js do end def write_parallel_comment(line, **params) - find("td[id='#{line}']").hover - find(".is-over button").click + find("div[id='#{line}']").hover + find(".js-add-diff-note-button").click write_comment(selector: "form[data-line-code='#{line}']", **params) end diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 0fd140a00bd..54c3fe738d2 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -132,7 +132,7 @@ RSpec.describe 'User comments on a diff', :js do # In `files/ruby/popen.rb` it 'allows comments for changes involving both sides' do # click +15, select -13 add and verify comment - click_diff_line(find('div[data-path="files/ruby/popen.rb"] .new_line a[data-linenumber="15"]').find(:xpath, '../..'), 'right') + click_diff_line(find('div[data-path="files/ruby/popen.rb"] .right-side a[data-linenumber="15"]').find(:xpath, '../../..'), 'right') add_comment('-13', '+15') end @@ -141,7 +141,7 @@ RSpec.describe 'User comments on a diff', :js do page.within('[data-path="files/ruby/popen.rb"]') do all('.js-unfold-all')[0].click end - click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="9"]').find(:xpath, '../..'), 'left') + click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="9"]').find(:xpath, '../..'), 'left') add_comment('1', '-9') end @@ -150,7 +150,7 @@ RSpec.describe 'User comments on a diff', :js do page.within('[data-path="files/ruby/popen.rb"]') do all('.js-unfold-all')[1].click end - click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="21"]').find(:xpath, '../..'), 'left') + click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="21"]').find(:xpath, '../..'), 'left') add_comment('18', '21') end @@ -159,7 +159,7 @@ RSpec.describe 'User comments on a diff', :js do page.within('[data-path="files/ruby/popen.rb"]') do all('.js-unfold-down')[1].click end - click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="30"]').find(:xpath, '../..'), 'left') + click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="30"]').find(:xpath, '../..'), 'left') add_comment('+28', '37') end end diff --git a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb index ac0c66524f0..3665ad91dd6 100644 --- a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb +++ b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb @@ -16,14 +16,14 @@ RSpec.describe 'Batch diffs', :js do wait_for_requests # Add discussion to first line of first file - click_diff_line(find('.diff-file.file-holder:first-of-type tr.line_holder.new:first-of-type')) + click_diff_line(find('.diff-file.file-holder:first-of-type .line_holder .left-side:first-of-type')) page.within('.js-discussion-note-form') do fill_in('note_note', with: 'First Line Comment') click_button('Add comment now') end # Add discussion to first line of last file - click_diff_line(find('.diff-file.file-holder:last-of-type tr.line_holder.new:first-of-type')) + click_diff_line(find('.diff-file.file-holder:last-of-type .line_holder .left-side:first-of-type')) page.within('.js-discussion-note-form') do fill_in('note_note', with: 'Last Line Comment') click_button('Add comment now') diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index 163ce10132e..c339a7d9976 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do let(:user) { project.creator } let(:comment_button_class) { '.add-diff-note' } let(:notes_holder_input_class) { 'js-temp-notes-holder' } - let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } + let(:notes_holder_input_xpath) { '..//following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' } let(:test_note_comment) { 'this is a test note!' } before do @@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do context 'with an old line on the left and no line on the right' do it 'allows commenting on the left side' do - should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left') + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]'), 'left') end it 'does not allow commenting on the right side' do @@ -67,7 +67,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do context 'with a match line' do it 'does not allow commenting' do - line_holder = find('.match', match: :first).find(:xpath, '..') + line_holder = find('.match', match: :first) match_should_not_allow_commenting(line_holder) end end @@ -81,17 +81,13 @@ RSpec.describe 'Merge request > User posts diff notes', :js do wait_for_requests end - # The first `.js-unfold` unfolds upwards, therefore the first - # `.line_holder` will be an unfolded line. - let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') } - it 'allows commenting on the left side' do - should_allow_commenting(line_holder, 'left') + should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="left-side"]')) end it 'allows commenting on the right side' do # Automatically shifts comment box to left side. - should_allow_commenting(line_holder, 'right') + should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="right-side"]')) end end end @@ -149,7 +145,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do # The first `.js-unfold` unfolds upwards, therefore the first # `.line_holder` will be an unfolded line. - let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') } + let(:line_holder) { first('[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') } it 'allows commenting' do should_allow_commenting line_holder diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index 34ae082750b..d0d09e678c6 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -30,8 +30,8 @@ RSpec.describe 'Merge request > User sees versions', :js do line_code = "#{file_id}_#{line_code}" page.within(diff_file_selector) do - find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover - find(".line_holder[id='#{line_code}'] button").click + first("[id='#{line_code}']").hover + first("[id='#{line_code}'] [role='button']").click page.within("form[data-line-code='#{line_code}']") do fill_in "note[note]", with: comment diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb index d5061657c59..09dfe41a718 100644 --- a/spec/features/merge_request/user_views_diffs_spec.rb +++ b/spec/features/merge_request/user_views_diffs_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'User views diffs', :js do page.within('.file-holder[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do expect(find('.text-file')).to have_content('fileutils') - expect(page).to have_selector('.new_line [data-linenumber="1"]', count: 1) + expect(page).to have_selector('[data-interop-type="new"] [data-linenumber="1"]') end end @@ -32,8 +32,8 @@ RSpec.describe 'User views diffs', :js do page.within('.file-holder[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do all('.js-unfold-all')[1].click - expect(page).to have_selector('.new_line [data-linenumber="24"]', count: 1) - expect(page).not_to have_selector('.new_line [data-linenumber="1"]') + expect(page).to have_selector('[data-interop-type="new"] [data-linenumber="24"]', count: 1) + expect(page).not_to have_selector('[data-interop-type="new"] [data-linenumber="1"]') end end diff --git a/spec/fixtures/packages/nuget/package.snupkg b/spec/fixtures/packages/nuget/package.snupkg Binary files differnew file mode 100644 index 00000000000..9d97b36e792 --- /dev/null +++ b/spec/fixtures/packages/nuget/package.snupkg diff --git a/spec/fixtures/packages/nuget/with_package_types.nuspec b/spec/fixtures/packages/nuget/with_package_types.nuspec new file mode 100644 index 00000000000..b0e61e9d47e --- /dev/null +++ b/spec/fixtures/packages/nuget/with_package_types.nuspec @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata> + <id>Test.Package</id> + <version>3.5.2</version> + <authors>Test Author</authors> + <owners>Test Owner</owners> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <description>Package Description</description> + <packageTypes> + <packageType name="SymbolsPackage" /> + </packageTypes> + </metadata> +</package> diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js index cd0eda2ab49..42990334f0a 100644 --- a/spec/frontend/clusters/clusters_bundle_spec.js +++ b/spec/frontend/clusters/clusters_bundle_spec.js @@ -2,15 +2,12 @@ import MockAdapter from 'axios-mock-adapter'; import { loadHTMLFixture } from 'helpers/fixtures'; import { setTestTimeout } from 'helpers/timeout'; import Clusters from '~/clusters/clusters_bundle'; -import { APPLICATION_STATUS, APPLICATIONS, RUNNER } from '~/clusters/constants'; import axios from '~/lib/utils/axios_utils'; import initProjectSelectDropdown from '~/project_select'; jest.mock('~/lib/utils/poll'); jest.mock('~/project_select'); -const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS; - describe('Clusters', () => { setTestTimeout(1000); @@ -57,67 +54,6 @@ describe('Clusters', () => { }); }); - describe('checkForNewInstalls', () => { - const INITIAL_APP_MAP = { - helm: { status: null, title: 'Helm Tiller' }, - ingress: { status: null, title: 'Ingress' }, - runner: { status: null, title: 'GitLab Runner' }, - }; - - it('does not show alert when things transition from initial null state to something', () => { - cluster.checkForNewInstalls(INITIAL_APP_MAP, { - ...INITIAL_APP_MAP, - helm: { status: INSTALLABLE, title: 'Helm Tiller' }, - }); - - const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); - - expect(flashMessage).toBeNull(); - }); - - it('shows an alert when something gets newly installed', () => { - cluster.checkForNewInstalls( - { - ...INITIAL_APP_MAP, - helm: { status: INSTALLING, title: 'Helm Tiller' }, - }, - { - ...INITIAL_APP_MAP, - helm: { status: INSTALLED, title: 'Helm Tiller' }, - }, - ); - - const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); - - expect(flashMessage).not.toBeNull(); - expect(flashMessage.textContent.trim()).toEqual( - 'Helm Tiller was successfully installed on your Kubernetes cluster', - ); - }); - - it('shows an alert when multiple things gets newly installed', () => { - cluster.checkForNewInstalls( - { - ...INITIAL_APP_MAP, - helm: { status: INSTALLING, title: 'Helm Tiller' }, - ingress: { status: INSTALLABLE, title: 'Ingress' }, - }, - { - ...INITIAL_APP_MAP, - helm: { status: INSTALLED, title: 'Helm Tiller' }, - ingress: { status: INSTALLED, title: 'Ingress' }, - }, - ); - - const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text'); - - expect(flashMessage).not.toBeNull(); - expect(flashMessage.textContent.trim()).toEqual( - 'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster', - ); - }); - }); - describe('updateContainer', () => { const { location } = window; @@ -237,77 +173,6 @@ describe('Clusters', () => { }); }); - describe('installApplication', () => { - it.each(APPLICATIONS)('tries to install %s', (applicationId, done) => { - jest.spyOn(cluster.service, 'installApplication').mockResolvedValue(); - - cluster.store.state.applications[applicationId].status = INSTALLABLE; - - const params = {}; - if (applicationId === 'knative') { - params.hostname = 'test-example.com'; - } - - // eslint-disable-next-line promise/valid-params - cluster - .installApplication({ id: applicationId, params }) - .then(() => { - expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING); - expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null); - expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, params); - done(); - }) - .catch(); - }); - - it('sets error request status when the request fails', () => { - jest - .spyOn(cluster.service, 'installApplication') - .mockRejectedValueOnce(new Error('STUBBED ERROR')); - - cluster.store.state.applications.helm.status = INSTALLABLE; - - const promise = cluster.installApplication({ id: 'helm' }); - - return promise.then(() => { - expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE); - expect(cluster.store.state.applications.helm.installFailed).toBe(true); - - expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); - }); - }); - }); - - describe('uninstallApplication', () => { - it.each(APPLICATIONS)('tries to uninstall %s', (applicationId) => { - jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce(); - - cluster.store.state.applications[applicationId].status = INSTALLED; - - cluster.uninstallApplication({ id: applicationId }); - - expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING); - expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null); - expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId); - }); - - it('sets error request status when the uninstall request fails', () => { - jest - .spyOn(cluster.service, 'uninstallApplication') - .mockRejectedValueOnce(new Error('STUBBED ERROR')); - - cluster.store.state.applications.helm.status = INSTALLED; - - const promise = cluster.uninstallApplication({ id: 'helm' }); - - return promise.then(() => { - expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED); - expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true); - expect(cluster.store.state.applications.helm.requestReason).toBeDefined(); - }); - }); - }); - describe('fetch cluster environments success', () => { beforeEach(() => { jest.spyOn(cluster.store, 'toggleFetchEnvironments').mockReturnThis(); @@ -328,7 +193,6 @@ describe('Clusters', () => { describe('handleClusterStatusSuccess', () => { beforeEach(() => { jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis(); - jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis(); jest.spyOn(cluster, 'updateContainer').mockReturnThis(); cluster.handleClusterStatusSuccess({ data: {} }); }); @@ -337,38 +201,8 @@ describe('Clusters', () => { expect(cluster.store.updateStateFromServer).toHaveBeenCalled(); }); - it('checks for new installable apps', () => { - expect(cluster.checkForNewInstalls).toHaveBeenCalled(); - }); - it('updates message containers', () => { expect(cluster.updateContainer).toHaveBeenCalled(); }); }); - - describe('updateApplication', () => { - const params = { version: '1.0.0' }; - let storeUpdateApplication; - let installApplication; - - beforeEach(() => { - storeUpdateApplication = jest.spyOn(cluster.store, 'updateApplication'); - installApplication = jest.spyOn(cluster.service, 'installApplication'); - - cluster.updateApplication({ id: RUNNER, params }); - }); - - afterEach(() => { - storeUpdateApplication.mockRestore(); - installApplication.mockRestore(); - }); - - it('calls store updateApplication method', () => { - expect(storeUpdateApplication).toHaveBeenCalledWith(RUNNER); - }); - - it('sends installApplication request', () => { - expect(installApplication).toHaveBeenCalledWith(RUNNER, params); - }); - }); }); diff --git a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap deleted file mode 100644 index c2ace1b4e30..00000000000 --- a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap +++ /dev/null @@ -1,105 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Applications Cert-Manager application shows the correct description 1`] = ` -<p - data-testid="certManagerDescription" -> - Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by - <a - class="gl-link" - href="https://letsencrypt.org/" - rel="noopener noreferrer" - target="_blank" - > - Let's Encrypt - </a> - and ensure that certificates are valid and up-to-date. -</p> -`; - -exports[`Applications Cilium application shows the correct description 1`] = ` -<p - data-testid="ciliumDescription" -> - Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. - <a - class="gl-link" - href="cilium-help-path" - rel="noopener" - target="_blank" - > - Learn more about configuring Network Policies here. - </a> -</p> -`; - -exports[`Applications Crossplane application shows the correct description 1`] = ` -<p - data-testid="crossplaneDescription" -> - Crossplane enables declarative provisioning of managed services from your cloud of choice using - <code> - kubectl - </code> - or - <a - class="gl-link" - href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane" - rel="noopener noreferrer" - target="_blank" - > - GitLab Integration - </a> - . Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on. -</p> -`; - -exports[`Applications Ingress application shows the correct warning message 1`] = ` -<span - data-testid="ingressCostWarning" -> - Installing Ingress may incur additional costs. Learn more about - <a - class="gl-link" - href="https://cloud.google.com/compute/pricing#lb" - rel="noopener noreferrer" - target="_blank" - > - pricing - </a> - . -</span> -`; - -exports[`Applications Knative application shows the correct description 1`] = ` -<span - data-testid="installed-via" -> - installed via - <a - class="gl-link" - href="" - rel="noopener" - target="_blank" - > - Cloud Run - </a> -</span> -`; - -exports[`Applications Prometheus application shows the correct description 1`] = ` -<span - data-testid="prometheusDescription" -> - Prometheus is an open-source monitoring system with - <a - class="gl-link" - href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html" - rel="noopener noreferrer" - target="_blank" - > - GitLab Integration - </a> - to monitor deployed applications. -</span> -`; diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js deleted file mode 100644 index 6bad1db542b..00000000000 --- a/spec/frontend/clusters/components/application_row_spec.js +++ /dev/null @@ -1,505 +0,0 @@ -import { GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import ApplicationRow from '~/clusters/components/application_row.vue'; -import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue'; -import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue'; -import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants'; -import eventHub from '~/clusters/event_hub'; - -import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; - -describe('Application Row', () => { - let wrapper; - - afterEach(() => { - wrapper.destroy(); - }); - - const mountComponent = (data) => { - wrapper = shallowMount(ApplicationRow, { - stubs: { GlSprintf }, - propsData: { - ...DEFAULT_APPLICATION_STATE, - ...data, - }, - }); - }; - - describe('Title', () => { - it('shows title', () => { - mountComponent({ titleLink: null }); - - const title = wrapper.find('.js-cluster-application-title'); - - expect(title.element).toBeInstanceOf(HTMLSpanElement); - expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title); - }); - - it('shows title link', () => { - expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined(); - mountComponent(); - const title = wrapper.find('.js-cluster-application-title'); - - expect(title.element).toBeInstanceOf(HTMLAnchorElement); - expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title); - }); - }); - - describe('Install button', () => { - const button = () => wrapper.find('.js-cluster-application-install-button'); - const checkButtonState = (label, loading, disabled) => { - expect(button().text()).toEqual(label); - expect(button().props('loading')).toEqual(loading); - expect(button().props('disabled')).toEqual(disabled); - }; - - it('has indeterminate state on page load', () => { - mountComponent({ status: null }); - - expect(button().text()).toBe(''); - }); - - it('has install button', () => { - mountComponent(); - - expect(button().exists()).toBe(true); - }); - - it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => { - mountComponent({ status: APPLICATION_STATUS.NOT_INSTALLABLE }); - - checkButtonState('Install', false, true); - }); - - it('has enabled "Install" when APPLICATION_STATUS.INSTALLABLE', () => { - mountComponent({ status: APPLICATION_STATUS.INSTALLABLE }); - - checkButtonState('Install', false, false); - }); - - it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => { - mountComponent({ status: APPLICATION_STATUS.INSTALLING }); - - checkButtonState('Installing', true, true); - }); - - it('has disabled "Install" when APPLICATION_STATUS.UNINSTALLED', () => { - mountComponent({ status: APPLICATION_STATUS.UNINSTALLED }); - - checkButtonState('Install', false, true); - }); - - it('has disabled "Externally installed" when APPLICATION_STATUS.EXTERNALLY_INSTALLED', () => { - mountComponent({ status: APPLICATION_STATUS.EXTERNALLY_INSTALLED }); - - checkButtonState('Externally installed', false, true); - }); - - it('has disabled "Installed" when application is installed and not uninstallable', () => { - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - installed: true, - uninstallable: false, - }); - - checkButtonState('Installed', false, true); - }); - - it('hides when application is installed and uninstallable', () => { - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - installed: true, - uninstallable: true, - }); - - expect(button().exists()).toBe(false); - }); - - it('has enabled "Install" when install fails', () => { - mountComponent({ - status: APPLICATION_STATUS.INSTALLABLE, - installFailed: true, - }); - - checkButtonState('Install', false, false); - }); - - it('has disabled "Install" when installation disabled', () => { - mountComponent({ - status: APPLICATION_STATUS.INSTALLABLE, - installable: false, - }); - - checkButtonState('Install', false, true); - }); - - it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => { - mountComponent({ status: APPLICATION_STATUS.INSTALLABLE }); - - checkButtonState('Install', false, false); - }); - - it('clicking install button emits event', () => { - const spy = jest.spyOn(eventHub, '$emit'); - mountComponent({ status: APPLICATION_STATUS.INSTALLABLE }); - - button().vm.$emit('click'); - - expect(spy).toHaveBeenCalledWith('installApplication', { - id: DEFAULT_APPLICATION_STATE.id, - params: {}, - }); - }); - - it('clicking install button when installApplicationRequestParams are provided emits event', () => { - const spy = jest.spyOn(eventHub, '$emit'); - mountComponent({ - status: APPLICATION_STATUS.INSTALLABLE, - installApplicationRequestParams: { hostname: 'jupyter' }, - }); - - button().vm.$emit('click'); - - expect(spy).toHaveBeenCalledWith('installApplication', { - id: DEFAULT_APPLICATION_STATE.id, - params: { hostname: 'jupyter' }, - }); - }); - - it('clicking disabled install button emits nothing', () => { - const spy = jest.spyOn(eventHub, '$emit'); - mountComponent({ status: APPLICATION_STATUS.INSTALLING }); - - expect(button().props('disabled')).toEqual(true); - - button().vm.$emit('click'); - - expect(spy).not.toHaveBeenCalled(); - }); - }); - - describe('Uninstall button', () => { - it('displays button when app is installed and uninstallable', () => { - mountComponent({ - installed: true, - uninstallable: true, - status: APPLICATION_STATUS.NOT_INSTALLABLE, - }); - const uninstallButton = wrapper.find('.js-cluster-application-uninstall-button'); - - expect(uninstallButton.exists()).toBe(true); - }); - - it('displays a success toast message if application uninstall was successful', async () => { - mountComponent({ - title: 'GitLab Runner', - uninstallSuccessful: false, - }); - - wrapper.vm.$toast = { show: jest.fn() }; - wrapper.setProps({ uninstallSuccessful: true }); - - await wrapper.vm.$nextTick(); - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith( - 'GitLab Runner uninstalled successfully.', - ); - }); - }); - - describe('when confirmation modal triggers confirm event', () => { - it('triggers uninstallApplication event', () => { - jest.spyOn(eventHub, '$emit'); - mountComponent(); - wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm'); - - expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', { - id: DEFAULT_APPLICATION_STATE.id, - }); - }); - }); - - describe('Update button', () => { - const button = () => wrapper.find('.js-cluster-application-update-button'); - - it('has indeterminate state on page load', () => { - mountComponent(); - - expect(button().exists()).toBe(false); - }); - - it('has enabled "Update" when "updateAvailable" is true', () => { - mountComponent({ updateAvailable: true }); - - expect(button().exists()).toBe(true); - expect(button().text()).toContain('Update'); - }); - - it('has enabled "Retry update" when update process fails', () => { - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - updateFailed: true, - }); - - expect(button().exists()).toBe(true); - expect(button().text()).toContain('Retry update'); - }); - - it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => { - mountComponent({ status: APPLICATION_STATUS.UPDATING }); - - expect(button().exists()).toBe(true); - expect(button().text()).toContain('Updating'); - }); - - it('clicking update button emits event', () => { - const spy = jest.spyOn(eventHub, '$emit'); - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - updateAvailable: true, - }); - - button().vm.$emit('click'); - - expect(spy).toHaveBeenCalledWith('updateApplication', { - id: DEFAULT_APPLICATION_STATE.id, - params: {}, - }); - }); - - it('clicking disabled update button emits nothing', () => { - const spy = jest.spyOn(eventHub, '$emit'); - mountComponent({ status: APPLICATION_STATUS.UPDATING }); - - button().vm.$emit('click'); - - expect(spy).not.toHaveBeenCalled(); - }); - - it('displays an error message if application update failed', () => { - mountComponent({ - title: 'GitLab Runner', - status: APPLICATION_STATUS.INSTALLED, - updateFailed: true, - }); - const failureMessage = wrapper.find('.js-cluster-application-update-details'); - - expect(failureMessage.exists()).toBe(true); - expect(failureMessage.text()).toContain( - 'Update failed. Please check the logs and try again.', - ); - }); - - it('displays a success toast message if application update was successful', async () => { - mountComponent({ - title: 'GitLab Runner', - updateSuccessful: false, - }); - - wrapper.vm.$toast = { show: jest.fn() }; - wrapper.setProps({ updateSuccessful: true }); - - await wrapper.vm.$nextTick(); - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.'); - }); - - describe('when updating does not require confirmation', () => { - beforeEach(() => mountComponent({ updateAvailable: true })); - - it('the modal is not rendered', () => { - expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false); - }); - - it('the correct button is rendered', () => { - expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true); - }); - }); - - describe('when updating requires confirmation', () => { - beforeEach(() => { - mountComponent({ - updateAvailable: true, - id: ELASTIC_STACK, - version: '1.1.2', - }); - }); - - it('displays a modal', () => { - expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true); - }); - - it('the correct button is rendered', () => { - expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe( - true, - ); - }); - - it('triggers updateApplication event', () => { - jest.spyOn(eventHub, '$emit'); - wrapper.find(UpdateApplicationConfirmationModal).vm.$emit('confirm'); - - expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', { - id: ELASTIC_STACK, - params: {}, - }); - }); - }); - - describe('updating Elastic Stack special case', () => { - it('needs confirmation if version is lower than 3.0.0', () => { - mountComponent({ - updateAvailable: true, - id: ELASTIC_STACK, - version: '1.1.2', - }); - - expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe( - true, - ); - expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true); - }); - - it('does not need confirmation is version is 3.0.0', () => { - mountComponent({ - updateAvailable: true, - id: ELASTIC_STACK, - version: '3.0.0', - }); - - expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true); - expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false); - }); - - it('does not need confirmation if version is higher than 3.0.0', () => { - mountComponent({ - updateAvailable: true, - id: ELASTIC_STACK, - version: '5.2.1', - }); - - expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true); - expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false); - }); - }); - }); - - describe('Version', () => { - const updateDetails = () => wrapper.find('.js-cluster-application-update-details'); - const versionEl = () => wrapper.find('.js-cluster-application-update-version'); - - it('displays a version number if application has been updated', () => { - const version = '0.1.45'; - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - updateSuccessful: true, - version, - }); - - expect(updateDetails().text()).toBe(`Updated to chart v${version}`); - }); - - it('contains a link to the chart repo if application has been updated', () => { - const version = '0.1.45'; - const chartRepo = 'https://gitlab.com/gitlab-org/charts/gitlab-runner'; - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - updateSuccessful: true, - chartRepo, - version, - }); - - expect(versionEl().attributes('href')).toEqual(chartRepo); - expect(versionEl().props('target')).toEqual('_blank'); - }); - - it('does not display a version number if application update failed', () => { - const version = '0.1.45'; - mountComponent({ - status: APPLICATION_STATUS.INSTALLED, - updateFailed: true, - version, - }); - - expect(updateDetails().text()).toBe('Update failed'); - expect(versionEl().exists()).toBe(false); - }); - - it('displays updating when the application update is currently updating', () => { - mountComponent({ - status: APPLICATION_STATUS.UPDATING, - updateSuccessful: true, - version: '1.2.3', - }); - - expect(updateDetails().text()).toBe('Updating'); - expect(versionEl().exists()).toBe(false); - }); - }); - - describe('Error block', () => { - const generalErrorMessage = () => wrapper.find('.js-cluster-application-general-error-message'); - - describe('when nothing fails', () => { - it('does not show error block', () => { - mountComponent(); - - expect(generalErrorMessage().exists()).toBe(false); - }); - }); - - describe('when install or uninstall fails', () => { - const statusReason = 'We broke it 0.0'; - const requestReason = 'We broke the request 0.0'; - - beforeEach(() => { - mountComponent({ - status: APPLICATION_STATUS.ERROR, - statusReason, - requestReason, - installFailed: true, - }); - }); - - it('shows status reason if it is available', () => { - const statusErrorMessage = wrapper.find('.js-cluster-application-status-error-message'); - - expect(statusErrorMessage.text()).toEqual(statusReason); - }); - - it('shows request reason if it is available', () => { - const requestErrorMessage = wrapper.find('.js-cluster-application-request-error-message'); - - expect(requestErrorMessage.text()).toEqual(requestReason); - }); - }); - - describe('when install fails', () => { - beforeEach(() => { - mountComponent({ - status: APPLICATION_STATUS.ERROR, - installFailed: true, - }); - }); - - it('shows a general message indicating the installation failed', () => { - expect(generalErrorMessage().text()).toEqual( - `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`, - ); - }); - }); - - describe('when uninstall fails', () => { - beforeEach(() => { - mountComponent({ - status: APPLICATION_STATUS.ERROR, - uninstallFailed: true, - }); - }); - - it('shows a general message indicating the uninstalling failed', () => { - expect(generalErrorMessage().text()).toEqual( - `Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`, - ); - }); - }); - }); -}); diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js deleted file mode 100644 index 511f5fc1d89..00000000000 --- a/spec/frontend/clusters/components/applications_spec.js +++ /dev/null @@ -1,510 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import ApplicationRow from '~/clusters/components/application_row.vue'; -import Applications from '~/clusters/components/applications.vue'; -import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue'; -import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue'; -import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants'; -import eventHub from '~/clusters/event_hub'; -import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; - -describe('Applications', () => { - let wrapper; - - beforeEach(() => { - gon.features = gon.features || {}; - }); - - const createComponent = ({ applications, type, propsData } = {}, isShallow) => { - const mountMethod = isShallow ? shallowMount : mount; - - wrapper = mountMethod(Applications, { - stubs: { ApplicationRow }, - propsData: { - type, - applications: { ...APPLICATIONS_MOCK_STATE, ...applications }, - ...propsData, - }, - }); - }; - - const createShallowComponent = (options) => createComponent(options, true); - const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`); - afterEach(() => { - wrapper.destroy(); - }); - - describe('Project cluster applications', () => { - beforeEach(() => { - createComponent({ type: CLUSTER_TYPE.PROJECT }); - }); - - it('renders a row for Ingress', () => { - expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true); - }); - - it('renders a row for Cert-Manager', () => { - expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true); - }); - - it('renders a row for Crossplane', () => { - expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true); - }); - - it('renders a row for Prometheus', () => { - expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true); - }); - - it('renders a row for GitLab Runner', () => { - expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true); - }); - - it('renders a row for Jupyter', () => { - expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true); - }); - - it('renders a row for Knative', () => { - expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true); - }); - - it('renders a row for Elastic Stack', () => { - expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true); - }); - - it('renders a row for Cilium', () => { - expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true); - }); - }); - - describe('Group cluster applications', () => { - beforeEach(() => { - createComponent({ type: CLUSTER_TYPE.GROUP }); - }); - - it('renders a row for Ingress', () => { - expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true); - }); - - it('renders a row for Cert-Manager', () => { - expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true); - }); - - it('renders a row for Crossplane', () => { - expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true); - }); - - it('renders a row for Prometheus', () => { - expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true); - }); - - it('renders a row for GitLab Runner', () => { - expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true); - }); - - it('renders a row for Jupyter', () => { - expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true); - }); - - it('renders a row for Knative', () => { - expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true); - }); - - it('renders a row for Elastic Stack', () => { - expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true); - }); - - it('renders a row for Cilium', () => { - expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true); - }); - }); - - describe('Instance cluster applications', () => { - beforeEach(() => { - createComponent({ type: CLUSTER_TYPE.INSTANCE }); - }); - - it('renders a row for Ingress', () => { - expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true); - }); - - it('renders a row for Cert-Manager', () => { - expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true); - }); - - it('renders a row for Crossplane', () => { - expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true); - }); - - it('renders a row for Prometheus', () => { - expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true); - }); - - it('renders a row for GitLab Runner', () => { - expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true); - }); - - it('renders a row for Jupyter', () => { - expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true); - }); - - it('renders a row for Knative', () => { - expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true); - }); - - it('renders a row for Elastic Stack', () => { - expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true); - }); - - it('renders a row for Cilium', () => { - expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true); - }); - }); - - describe('Helm application', () => { - it('does not render a row for Helm Tiller', () => { - createComponent(); - expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false); - }); - }); - - describe('Ingress application', () => { - it('shows the correct warning message', () => { - createComponent(); - expect(findByTestId('ingressCostWarning').element).toMatchSnapshot(); - }); - - describe('when installed', () => { - describe('with ip address', () => { - it('renders ip address with a clipboard button', () => { - createComponent({ - applications: { - ingress: { - title: 'Ingress', - status: 'installed', - externalIp: '0.0.0.0', - }, - }, - }); - - expect(wrapper.find('.js-endpoint').element.value).toEqual('0.0.0.0'); - expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual( - '0.0.0.0', - ); - }); - }); - - describe('with hostname', () => { - it('renders hostname with a clipboard button', () => { - createComponent({ - applications: { - ingress: { - title: 'Ingress', - status: 'installed', - externalHostname: 'localhost.localdomain', - }, - cert_manager: { title: 'Cert-Manager' }, - crossplane: { title: 'Crossplane', stack: '' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', hostname: '' }, - knative: { title: 'Knative', hostname: '' }, - elastic_stack: { title: 'Elastic Stack' }, - cilium: { title: 'GitLab Container Network Policies' }, - }, - }); - - expect(wrapper.find('.js-endpoint').element.value).toEqual('localhost.localdomain'); - - expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual( - 'localhost.localdomain', - ); - }); - }); - - describe('without ip address', () => { - it('renders an input text with a loading icon and an alert text', () => { - createComponent({ - applications: { - ingress: { - title: 'Ingress', - status: 'installed', - }, - }, - }); - - expect(wrapper.find('.js-ingress-ip-loading-icon').exists()).toBe(true); - expect(wrapper.find('.js-no-endpoint-message').exists()).toBe(true); - }); - }); - }); - - describe('before installing', () => { - it('does not render the IP address', () => { - createComponent(); - - expect(wrapper.text()).not.toContain('Ingress IP Address'); - expect(wrapper.find('.js-endpoint').exists()).toBe(false); - }); - }); - }); - - describe('Cert-Manager application', () => { - it('shows the correct description', () => { - createComponent(); - expect(findByTestId('certManagerDescription').element).toMatchSnapshot(); - }); - - describe('when not installed', () => { - it('renders email & allows editing', () => { - createComponent({ - applications: { - cert_manager: { - title: 'Cert-Manager', - email: 'before@example.com', - status: 'installable', - }, - }, - }); - - expect(wrapper.find('.js-email').element.value).toEqual('before@example.com'); - expect(wrapper.find('.js-email').attributes('readonly')).toBe(undefined); - }); - }); - - describe('when installed', () => { - it('renders email in readonly', () => { - createComponent({ - applications: { - cert_manager: { - title: 'Cert-Manager', - email: 'after@example.com', - status: 'installed', - }, - }, - }); - - expect(wrapper.find('.js-email').element.value).toEqual('after@example.com'); - expect(wrapper.find('.js-email').attributes('readonly')).toEqual('readonly'); - }); - }); - }); - - describe('Jupyter application', () => { - describe('with ingress installed with ip & jupyter installable', () => { - it('renders hostname active input', () => { - createComponent({ - applications: { - ingress: { - title: 'Ingress', - status: 'installed', - externalIp: '1.1.1.1', - }, - }, - }); - - expect( - wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'), - ).toEqual(undefined); - }); - }); - - describe('with ingress installed without external ip', () => { - it('does not render hostname input', () => { - createComponent({ - applications: { - ingress: { title: 'Ingress', status: 'installed' }, - }, - }); - - expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe( - false, - ); - }); - }); - - describe('with ingress & jupyter installed', () => { - it('renders readonly input', () => { - createComponent({ - applications: { - ingress: { - title: 'Ingress', - status: 'installed', - externalIp: '1.1.1.1', - }, - jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' }, - }, - }); - - expect( - wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'), - ).toEqual('readonly'); - }); - }); - - describe('without ingress installed', () => { - beforeEach(() => { - createComponent(); - }); - - it('does not render input', () => { - expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe( - false, - ); - }); - }); - }); - - describe('Prometheus application', () => { - it('shows the correct description', () => { - createComponent(); - expect(findByTestId('prometheusDescription').element).toMatchSnapshot(); - }); - }); - - describe('Knative application', () => { - const availableDomain = { - id: 4, - domain: 'newhostname.com', - }; - const propsData = { - applications: { - knative: { - title: 'Knative', - hostname: 'example.com', - status: 'installed', - externalIp: '1.1.1.1', - installed: true, - availableDomains: [availableDomain], - pagesDomain: null, - }, - }, - }; - let knativeDomainEditor; - - beforeEach(() => { - createShallowComponent(propsData); - jest.spyOn(eventHub, '$emit'); - - knativeDomainEditor = wrapper.find(KnativeDomainEditor); - }); - - it('shows the correct description', async () => { - createComponent(); - wrapper.setProps({ - providerType: PROVIDER_TYPE.GCP, - preInstalledKnative: true, - }); - - await wrapper.vm.$nextTick(); - - expect(findByTestId('installed-via').element).toMatchSnapshot(); - }); - - it('emits saveKnativeDomain event when knative domain editor emits save event', () => { - propsData.applications.knative.hostname = availableDomain.domain; - propsData.applications.knative.pagesDomain = availableDomain; - knativeDomainEditor.vm.$emit('save'); - - expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', { - id: 'knative', - params: { - hostname: availableDomain.domain, - pages_domain_id: availableDomain.id, - }, - }); - }); - - it('emits saveKnativeDomain event when knative domain editor emits save event with custom domain', () => { - const newHostName = 'someothernewhostname.com'; - propsData.applications.knative.hostname = newHostName; - propsData.applications.knative.pagesDomain = null; - knativeDomainEditor.vm.$emit('save'); - - expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', { - id: 'knative', - params: { - hostname: newHostName, - pages_domain_id: undefined, - }, - }); - }); - - it('emits setKnativeHostname event when knative domain editor emits change event', () => { - wrapper.find(KnativeDomainEditor).vm.$emit('set', { - domain: availableDomain.domain, - domainId: availableDomain.id, - }); - - expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeDomain', { - id: 'knative', - domain: availableDomain.domain, - domainId: availableDomain.id, - }); - }); - }); - - describe('Crossplane application', () => { - const propsData = { - applications: { - crossplane: { - title: 'Crossplane', - stack: { - code: '', - }, - }, - }, - }; - - beforeEach(() => createShallowComponent(propsData)); - - it('renders the correct Component', () => { - const crossplane = wrapper.find(CrossplaneProviderStack); - expect(crossplane.exists()).toBe(true); - }); - - it('shows the correct description', () => { - createComponent(); - expect(findByTestId('crossplaneDescription').element).toMatchSnapshot(); - }); - }); - - describe('Elastic Stack application', () => { - describe('with elastic stack installable', () => { - it('renders the install button enabled', () => { - createComponent(); - - expect( - wrapper - .find( - '.js-cluster-application-row-elastic_stack .js-cluster-application-install-button', - ) - .attributes('disabled'), - ).toBeUndefined(); - }); - }); - - describe('elastic stack installed', () => { - it('renders uninstall button', () => { - createComponent({ - applications: { - elastic_stack: { title: 'Elastic Stack', status: 'installed' }, - }, - }); - - expect( - wrapper - .find( - '.js-cluster-application-row-elastic_stack .js-cluster-application-install-button', - ) - .attributes('disabled'), - ).toEqual('disabled'); - }); - }); - }); - - describe('Cilium application', () => { - it('shows the correct description', () => { - createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } }); - expect(findByTestId('ciliumDescription').element).toMatchSnapshot(); - }); - }); -}); diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js deleted file mode 100644 index 207eb071171..00000000000 --- a/spec/frontend/clusters/components/knative_domain_editor_spec.js +++ /dev/null @@ -1,179 +0,0 @@ -import { GlDropdownItem, GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue'; -import { APPLICATION_STATUS } from '~/clusters/constants'; - -const { UPDATING } = APPLICATION_STATUS; - -describe('KnativeDomainEditor', () => { - let wrapper; - let knative; - - const createComponent = (props = {}) => { - wrapper = shallowMount(KnativeDomainEditor, { - propsData: { ...props }, - }); - }; - - beforeEach(() => { - knative = { - title: 'Knative', - hostname: 'example.com', - installed: true, - }; - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('knative has an assigned IP address', () => { - beforeEach(() => { - knative.externalIp = '1.1.1.1'; - createComponent({ knative }); - }); - - it('renders ip address with a clipboard button', () => { - expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true); - expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp); - }); - - it('displays ip address clipboard button', () => { - expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual( - knative.externalIp, - ); - }); - - it('renders domain & allows editing', () => { - const domainNameInput = wrapper.find('.js-knative-domainname'); - - expect(domainNameInput.element.value).toEqual(knative.hostname); - expect(domainNameInput.attributes('readonly')).toBeFalsy(); - }); - - it('renders an update/save Knative domain button', () => { - expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true); - }); - }); - - describe('knative without ip address', () => { - beforeEach(() => { - knative.externalIp = null; - createComponent({ knative }); - }); - - it('renders an input text with a loading icon', () => { - expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true); - }); - - it('renders message indicating there is not IP address assigned', () => { - expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true); - }); - }); - - describe('clicking save changes button', () => { - beforeEach(() => { - createComponent({ knative }); - }); - - it('triggers save event and pass current knative hostname', () => { - wrapper.find(GlButton).vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('save').length).toEqual(1); - }); - }); - }); - - describe('when knative domain name was saved successfully', () => { - beforeEach(() => { - createComponent({ knative }); - }); - - it('displays toast indicating a successful update', () => { - wrapper.vm.$toast = { show: jest.fn() }; - wrapper.setProps({ knative: { updateSuccessful: true, ...knative } }); - - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith( - 'Knative domain name was updated successfully.', - ); - }); - }); - }); - - describe('when knative domain name input changes', () => { - it('emits "set" event with updated domain name', () => { - const newDomain = { - id: 4, - domain: 'newhostname.com', - }; - - createComponent({ knative: { ...knative, availableDomains: [newDomain] } }); - jest.spyOn(wrapper.vm, 'selectDomain'); - - wrapper.find(GlDropdownItem).vm.$emit('click'); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.selectDomain).toHaveBeenCalledWith(newDomain); - expect(wrapper.emitted('set')[0]).toEqual([ - { - domain: newDomain.domain, - domainId: newDomain.id, - }, - ]); - }); - }); - - it('emits "set" event with updated custom domain name', () => { - const newHostname = 'newhostname.com'; - - createComponent({ knative }); - jest.spyOn(wrapper.vm, 'selectCustomDomain'); - - wrapper.setData({ knativeHostname: newHostname }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.vm.selectCustomDomain).toHaveBeenCalledWith(newHostname); - expect(wrapper.emitted('set')[0]).toEqual([ - { - domain: newHostname, - domainId: null, - }, - ]); - }); - }); - }); - - describe('when updating knative domain name failed', () => { - beforeEach(() => { - createComponent({ knative }); - }); - - it('displays an error banner indicating the operation failure', () => { - wrapper.setProps({ knative: { updateFailed: true, ...knative } }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true); - }); - }); - }); - - describe(`when knative status is ${UPDATING}`, () => { - beforeEach(() => { - createComponent({ knative: { status: UPDATING, ...knative } }); - }); - - it('renders loading spinner in save button', () => { - expect(wrapper.find(GlButton).props('loading')).toBe(true); - }); - - it('renders disabled save button', () => { - expect(wrapper.find(GlButton).props('disabled')).toBe(true); - }); - - it('renders save button with "Saving" label', () => { - expect(wrapper.find(GlButton).text()).toBe('Saving'); - }); - }); -}); diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js deleted file mode 100644 index 2596820e5ac..00000000000 --- a/spec/frontend/clusters/components/uninstall_application_button_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue'; -import { APPLICATION_STATUS } from '~/clusters/constants'; - -const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS; - -describe('UninstallApplicationButton', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMount(UninstallApplicationButton, { - propsData: { ...props }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe.each` - status | loading | disabled | text - ${INSTALLED} | ${false} | ${false} | ${'Uninstall'} - ${UPDATING} | ${false} | ${true} | ${'Uninstall'} - ${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'} - `('when app status is $status', ({ loading, disabled, status, text }) => { - beforeEach(() => { - createComponent({ status }); - }); - - it(`renders a button with loading=${loading} and disabled=${disabled}`, () => { - expect(wrapper.find(GlButton).props()).toMatchObject({ loading, disabled }); - }); - - it(`renders a button with text="${text}"`, () => { - expect(wrapper.find(GlButton).text()).toBe(text); - }); - }); -}); diff --git a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js deleted file mode 100644 index 74ae4ecc486..00000000000 --- a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue'; -import { INGRESS } from '~/clusters/constants'; - -describe('UninstallApplicationConfirmationModal', () => { - let wrapper; - const appTitle = 'Ingress'; - - const createComponent = (props = {}) => { - wrapper = shallowMount(UninstallApplicationConfirmationModal, { - propsData: { ...props }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - beforeEach(() => { - createComponent({ application: INGRESS, applicationTitle: appTitle }); - }); - - it(`renders a modal with a title "Uninstall ${appTitle}"`, () => { - expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`); - }); - - it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => { - expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`); - }); - - describe('when ok button is clicked', () => { - beforeEach(() => { - jest.spyOn(wrapper.vm, 'trackUninstallButtonClick'); - wrapper.find(GlModal).vm.$emit('ok'); - }); - - it('emits confirm event', () => - wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('confirm')).toBeTruthy(); - })); - - it('calls track uninstall button click mixin', () => { - expect(wrapper.vm.trackUninstallButtonClick).toHaveBeenCalledWith(INGRESS); - }); - }); - - it('displays a warning text indicating the app will be uninstalled', () => { - expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`); - }); - - it('displays a custom warning text depending on the application', () => { - expect(wrapper.text()).toContain( - `The associated load balancer and IP will be deleted and cannot be restored.`, - ); - }); -}); diff --git a/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js deleted file mode 100644 index e933f17a980..00000000000 --- a/spec/frontend/clusters/components/update_application_confirmation_modal_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue'; -import { ELASTIC_STACK } from '~/clusters/constants'; - -describe('UpdateApplicationConfirmationModal', () => { - let wrapper; - const appTitle = 'Elastic stack'; - - const createComponent = (props = {}) => { - wrapper = shallowMount(UpdateApplicationConfirmationModal, { - propsData: { ...props }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - beforeEach(() => { - createComponent({ application: ELASTIC_STACK, applicationTitle: appTitle }); - }); - - it(`renders a modal with a title "Update ${appTitle}"`, () => { - expect(wrapper.find(GlModal).attributes('title')).toEqual(`Update ${appTitle}`); - }); - - it(`renders a modal with an ok button labeled "Update ${appTitle}"`, () => { - expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Update ${appTitle}`); - }); - - describe('when ok button is clicked', () => { - beforeEach(() => { - wrapper.find(GlModal).vm.$emit('ok'); - }); - - it('emits confirm event', () => - wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted('confirm')).toBeTruthy(); - })); - - it('displays a warning text indicating the app will be updated', () => { - expect(wrapper.text()).toContain(`You are about to update ${appTitle} on your cluster.`); - }); - - it('displays a custom warning text depending on the application', () => { - expect(wrapper.text()).toContain( - `Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.`, - ); - }); - }); -}); diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js deleted file mode 100644 index 4e731e331c2..00000000000 --- a/spec/frontend/clusters/services/application_state_machine_spec.js +++ /dev/null @@ -1,206 +0,0 @@ -import { - APPLICATION_STATUS, - UNINSTALL_EVENT, - UPDATE_EVENT, - INSTALL_EVENT, -} from '~/clusters/constants'; -import transitionApplicationState from '~/clusters/services/application_state_machine'; - -const { - NO_STATUS, - SCHEDULED, - NOT_INSTALLABLE, - INSTALLABLE, - INSTALLING, - INSTALLED, - ERROR, - UPDATING, - UPDATED, - UPDATE_ERRORED, - UNINSTALLING, - UNINSTALL_ERRORED, - UNINSTALLED, - PRE_INSTALLED, - EXTERNALLY_INSTALLED, -} = APPLICATION_STATUS; - -const NO_EFFECTS = 'no effects'; - -describe('applicationStateMachine', () => { - const noEffectsToEmptyObject = (effects) => (typeof effects === 'string' ? {} : effects); - - describe(`current state is ${NO_STATUS}`, () => { - it.each` - expectedState | event | effects - ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS} - ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS} - ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS} - ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS} - ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS} - ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} - ${UPDATING} | ${UPDATING} | ${NO_EFFECTS} - ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS} - ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }} - ${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS} - ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }} - ${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS} - ${PRE_INSTALLED} | ${PRE_INSTALLED} | ${NO_EFFECTS} - ${EXTERNALLY_INSTALLED} | ${EXTERNALLY_INSTALLED} | ${NO_EFFECTS} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: NO_STATUS, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...noEffectsToEmptyObject(effects), - }); - }); - }); - - describe(`current state is ${NOT_INSTALLABLE}`, () => { - it.each` - expectedState | event | effects - ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: NOT_INSTALLABLE, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...noEffectsToEmptyObject(effects), - }); - }); - }); - - describe(`current state is ${INSTALLABLE}`, () => { - it.each` - expectedState | event | effects - ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }} - ${INSTALLED} | ${INSTALLED} | ${{ installFailed: false }} - ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS} - ${UNINSTALLED} | ${UNINSTALLED} | ${{ installFailed: false }} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: INSTALLABLE, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...noEffectsToEmptyObject(effects), - }); - }); - }); - - describe(`current state is ${INSTALLING}`, () => { - it.each` - expectedState | event | effects - ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS} - ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: INSTALLING, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...noEffectsToEmptyObject(effects), - }); - }); - }); - - describe(`current state is ${INSTALLED}`, () => { - it.each` - expectedState | event | effects - ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }} - ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }} - ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS} - ${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS} - ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: INSTALLED, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...noEffectsToEmptyObject(effects), - }); - }); - }); - - describe(`current state is ${UPDATING}`, () => { - it.each` - expectedState | event | effects - ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true }} - ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: UPDATING, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...effects, - }); - }); - }); - - describe(`current state is ${UNINSTALLING}`, () => { - it.each` - expectedState | event | effects - ${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }} - ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: UNINSTALLING, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...effects, - }); - }); - }); - - describe(`current state is ${UNINSTALLED}`, () => { - it.each` - expectedState | event | effects - ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS} - ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }} - `(`transitions to $expectedState on $event event and applies $effects`, (data) => { - const { expectedState, event, effects } = data; - const currentAppState = { - status: UNINSTALLED, - }; - - expect(transitionApplicationState(currentAppState, event)).toEqual({ - status: expectedState, - ...noEffectsToEmptyObject(effects), - }); - }); - }); - describe('current state is undefined', () => { - it('returns the current state without having any effects', () => { - const currentAppState = {}; - expect(transitionApplicationState(currentAppState, INSTALLABLE)).toEqual(currentAppState); - }); - }); - - describe('with event is undefined', () => { - it('returns the current state without having any effects', () => { - const currentAppState = { - status: NO_STATUS, - }; - expect(transitionApplicationState(currentAppState, undefined)).toEqual(currentAppState); - }); - }); -}); diff --git a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js deleted file mode 100644 index f95b175ca64..00000000000 --- a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { GlDropdownItem, GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue'; - -describe('CrossplaneProviderStack component', () => { - let wrapper; - - const defaultProps = { - stacks: [ - { - name: 'Google Cloud Platform', - code: 'gcp', - }, - { - name: 'Amazon Web Services', - code: 'aws', - }, - ], - }; - - function createComponent(props = {}) { - const propsData = { - ...defaultProps, - ...props, - }; - - wrapper = shallowMount(CrossplaneProviderStack, { - propsData, - }); - } - - beforeEach(() => { - const crossplane = { - title: 'crossplane', - stack: '', - }; - createComponent({ crossplane }); - }); - - const findDropdownElements = () => wrapper.findAll(GlDropdownItem); - const findFirstDropdownElement = () => findDropdownElements().at(0); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders all of the available stacks in the dropdown', () => { - const dropdownElements = findDropdownElements(); - - expect(dropdownElements.length).toBe(defaultProps.stacks.length); - - defaultProps.stacks.forEach((stack, index) => - expect(dropdownElements.at(index).text()).toEqual(stack.name), - ); - }); - - it('displays the correct label for the first dropdown item if a stack is selected', () => { - const crossplane = { - title: 'crossplane', - stack: 'gcp', - }; - createComponent({ crossplane }); - expect(wrapper.vm.dropdownText).toBe('Google Cloud Platform'); - }); - - it('emits the "set" event with the selected stack value', () => { - const crossplane = { - title: 'crossplane', - stack: 'gcp', - }; - createComponent({ crossplane }); - findFirstDropdownElement().vm.$emit('click'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().set[0][0].code).toEqual('gcp'); - }); - }); - - it('renders the correct dropdown text when no stack is selected', () => { - expect(wrapper.vm.dropdownText).toBe('Select Stack'); - }); - - it('renders an external link', () => { - expect(wrapper.find(GlIcon).props('name')).toBe('external-link'); - }); -}); diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js index a75fcb0cb06..cf63d5452ac 100644 --- a/spec/frontend/clusters/services/mock_data.js +++ b/spec/frontend/clusters/services/mock_data.js @@ -1,170 +1,19 @@ -import { APPLICATION_STATUS } from '~/clusters/constants'; - const CLUSTERS_MOCK_DATA = { GET: { '/gitlab-org/gitlab-shell/clusters/1/status.json': { data: { status: 'errored', status_reason: 'Failed to request to CloudPlatform.', - applications: [ - { - name: 'helm', - status: APPLICATION_STATUS.INSTALLABLE, - status_reason: null, - can_uninstall: false, - }, - { - name: 'ingress', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - external_ip: null, - external_hostname: null, - can_uninstall: false, - }, - { - name: 'runner', - status: APPLICATION_STATUS.INSTALLING, - status_reason: null, - can_uninstall: false, - }, - { - name: 'prometheus', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - can_uninstall: false, - }, - { - name: 'jupyter', - status: APPLICATION_STATUS.INSTALLING, - status_reason: 'Cannot connect', - can_uninstall: false, - }, - { - name: 'knative', - status: APPLICATION_STATUS.INSTALLING, - status_reason: 'Cannot connect', - can_uninstall: false, - }, - { - name: 'cert_manager', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - email: 'test@example.com', - can_uninstall: false, - }, - { - name: 'crossplane', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - can_uninstall: false, - }, - { - name: 'elastic_stack', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - can_uninstall: false, - }, - ], }, }, '/gitlab-org/gitlab-shell/clusters/2/status.json': { data: { status: 'errored', status_reason: 'Failed to request to CloudPlatform.', - applications: [ - { - name: 'helm', - status: APPLICATION_STATUS.INSTALLED, - status_reason: null, - }, - { - name: 'ingress', - status: APPLICATION_STATUS.INSTALLED, - status_reason: 'Cannot connect', - external_ip: '1.1.1.1', - external_hostname: null, - }, - { - name: 'runner', - status: APPLICATION_STATUS.INSTALLING, - status_reason: null, - }, - { - name: 'prometheus', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - }, - { - name: 'jupyter', - status: APPLICATION_STATUS.INSTALLABLE, - status_reason: 'Cannot connect', - }, - { - name: 'knative', - status: APPLICATION_STATUS.INSTALLABLE, - status_reason: 'Cannot connect', - }, - { - name: 'cert_manager', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - email: 'test@example.com', - }, - { - name: 'crossplane', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - stack: 'gcp', - }, - { - name: 'elastic_stack', - status: APPLICATION_STATUS.ERROR, - status_reason: 'Cannot connect', - }, - ], }, }, }, - POST: { - '/gitlab-org/gitlab-shell/clusters/1/applications/helm': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/crossplane': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/runner': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {}, - '/gitlab-org/gitlab-shell/clusters/1/applications/elastic_stack': {}, - }, -}; - -const DEFAULT_APPLICATION_STATE = { - id: 'some-app', - title: 'My App', - titleLink: 'https://about.gitlab.com/', - description: 'Some description about this interesting application!', - status: null, - statusReason: null, - requestReason: null, -}; - -const APPLICATIONS_MOCK_STATE = { - helm: { title: 'Helm Tiller', status: 'installable' }, - ingress: { - title: 'Ingress', - status: 'installable', - }, - crossplane: { title: 'Crossplane', status: 'installable', stack: '' }, - cert_manager: { title: 'Cert-Manager', status: 'installable' }, - runner: { title: 'GitLab Runner' }, - prometheus: { title: 'Prometheus' }, - jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' }, - knative: { title: 'Knative ', status: 'installable', hostname: '' }, - elastic_stack: { title: 'Elastic Stack', status: 'installable' }, - cilium: { - title: 'GitLab Container Network Policies', - status: 'not_installable', - }, + POST: {}, }; -export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE }; +export { CLUSTERS_MOCK_DATA }; diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index cdba6fc6ab8..5e797bbf8a8 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -1,4 +1,3 @@ -import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants'; import ClustersStore from '~/clusters/stores/clusters_store'; import { CLUSTERS_MOCK_DATA } from '../services/mock_data'; @@ -31,17 +30,6 @@ describe('Clusters Store', () => { }); }); - describe('updateAppProperty', () => { - it('should store new request reason', () => { - expect(store.state.applications.helm.requestReason).toEqual(null); - - const newReason = 'We broke it.'; - store.updateAppProperty('helm', 'requestReason', newReason); - - expect(store.state.applications.helm.requestReason).toEqual(newReason); - }); - }); - describe('updateStateFromServer', () => { it('should store new polling data from server', () => { const mockResponseData = @@ -50,196 +38,16 @@ describe('Clusters Store', () => { expect(store.state).toEqual({ helpPath: null, - helmHelpPath: null, - ingressHelpPath: null, environmentsHelpPath: null, clustersHelpPath: null, deployBoardsHelpPath: null, - cloudRunHelpPath: null, status: mockResponseData.status, statusReason: mockResponseData.status_reason, providerType: null, - preInstalledKnative: false, rbac: false, - applications: { - helm: { - title: 'Legacy Helm Tiller server', - status: mockResponseData.applications[0].status, - statusReason: mockResponseData.applications[0].status_reason, - requestReason: null, - installable: true, - installed: false, - installFailed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - ingress: { - title: 'Ingress', - status: APPLICATION_STATUS.INSTALLABLE, - statusReason: mockResponseData.applications[1].status_reason, - requestReason: null, - externalIp: null, - externalHostname: null, - installable: true, - installed: false, - installFailed: true, - uninstallable: false, - updateFailed: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - runner: { - title: 'GitLab Runner', - status: mockResponseData.applications[2].status, - statusReason: mockResponseData.applications[2].status_reason, - requestReason: null, - version: mockResponseData.applications[2].version, - updateAvailable: mockResponseData.applications[2].update_available, - chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner', - installable: true, - installed: false, - installFailed: false, - updateFailed: false, - updateSuccessful: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - prometheus: { - title: 'Prometheus', - status: APPLICATION_STATUS.INSTALLABLE, - statusReason: mockResponseData.applications[3].status_reason, - requestReason: null, - installable: true, - installed: false, - installFailed: true, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - jupyter: { - title: 'JupyterHub', - status: mockResponseData.applications[4].status, - statusReason: mockResponseData.applications[4].status_reason, - requestReason: null, - hostname: '', - installable: true, - installed: false, - installFailed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - knative: { - title: 'Knative', - status: mockResponseData.applications[5].status, - statusReason: mockResponseData.applications[5].status_reason, - requestReason: null, - hostname: null, - isEditingDomain: false, - externalIp: null, - externalHostname: null, - installable: true, - installed: false, - installFailed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - updateSuccessful: false, - updateFailed: false, - validationError: null, - }, - cert_manager: { - title: 'Cert-Manager', - status: APPLICATION_STATUS.INSTALLABLE, - installFailed: true, - statusReason: mockResponseData.applications[6].status_reason, - requestReason: null, - email: mockResponseData.applications[6].email, - installable: true, - installed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - elastic_stack: { - title: 'Elastic Stack', - status: APPLICATION_STATUS.INSTALLABLE, - installFailed: true, - statusReason: mockResponseData.applications[7].status_reason, - requestReason: null, - installable: true, - installed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - crossplane: { - title: 'Crossplane', - status: APPLICATION_STATUS.INSTALLABLE, - installFailed: true, - statusReason: mockResponseData.applications[8].status_reason, - requestReason: null, - installable: true, - installed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - cilium: { - title: 'GitLab Container Network Policies', - status: null, - statusReason: null, - requestReason: null, - installable: false, - installed: false, - installFailed: false, - uninstallable: false, - uninstallSuccessful: false, - uninstallFailed: false, - validationError: null, - }, - }, environments: [], fetchingEnvironments: false, }); }); - - describe.each(APPLICATION_INSTALLED_STATUSES)( - 'given the current app status is %s', - (status) => { - it('marks application as installed', () => { - const mockResponseData = - CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data; - const runnerAppIndex = 2; - - mockResponseData.applications[runnerAppIndex].status = status; - - store.updateStateFromServer(mockResponseData); - - expect(store.state.applications[RUNNER].installed).toBe(true); - }); - }, - ); - - it('sets default hostname for jupyter when ingress has a ip address', () => { - const mockResponseData = - CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data; - - store.updateStateFromServer(mockResponseData); - - expect(store.state.applications.jupyter.hostname).toEqual( - `jupyter.${store.state.applications.ingress.externalIp}.nip.io`, - ); - }); }); }); diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js index 7012889440c..0a7dfc02c65 100644 --- a/spec/frontend/diffs/components/diff_content_spec.js +++ b/spec/frontend/diffs/components/diff_content_spec.js @@ -4,8 +4,6 @@ import Vuex from 'vuex'; import DiffContentComponent from '~/diffs/components/diff_content.vue'; import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; import DiffView from '~/diffs/components/diff_view.vue'; -import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; -import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; import { diffViewerModes } from '~/ide/constants'; import NoteForm from '~/notes/components/note_form.vue'; @@ -107,25 +105,10 @@ describe('DiffContent', () => { }); const textDiffFile = { ...defaultProps.diffFile, viewer: { name: diffViewerModes.text } }; - it('should render diff inline view if `isInlineView` is true', () => { - isInlineViewGetterMock.mockReturnValue(true); - createComponent({ props: { diffFile: textDiffFile } }); - - expect(wrapper.find(InlineDiffView).exists()).toBe(true); - }); - - it('should render parallel view if `isParallelView` getter is true', () => { - isParallelViewGetterMock.mockReturnValue(true); - createComponent({ props: { diffFile: textDiffFile } }); - - expect(wrapper.find(ParallelDiffView).exists()).toBe(true); - }); it('should render diff view if `unifiedDiffComponents` are true', () => { - isParallelViewGetterMock.mockReturnValue(true); createComponent({ props: { diffFile: textDiffFile }, - provide: { glFeatures: { unifiedDiffComponents: true } }, }); expect(wrapper.find(DiffView).exists()).toBe(true); diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js index 47ae3cd5867..7cc94464585 100644 --- a/spec/frontend/diffs/components/diff_row_utils_spec.js +++ b/spec/frontend/diffs/components/diff_row_utils_spec.js @@ -258,30 +258,3 @@ describe('mapParallel', () => { expect(mapped.right).toMatchObject(rightExpectation); }); }); - -describe('mapInline', () => { - it('should assign computed properties to the line object', () => { - const content = { - diffFile: {}, - shouldRenderDraftRow: () => false, - }; - const line = { - discussions: [{}], - discussionsExpanded: true, - hasForm: true, - }; - const expectation = { - commentRowClasses: '', - hasDiscussions: true, - isContextLine: false, - isMatchLine: false, - isMetaLine: false, - renderDiscussion: true, - hasDraft: false, - hasCommentForm: true, - }; - const mapped = utils.mapInline(content)(line); - - expect(mapped).toMatchObject(expectation); - }); -}); diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js deleted file mode 100644 index 9c3e00cd6cf..00000000000 --- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js +++ /dev/null @@ -1,325 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; -import { mapInline } from '~/diffs/components/diff_row_utils'; -import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; -import { createStore } from '~/mr_notes/stores'; -import { findInteropAttributes } from '../find_interop_attributes'; -import discussionsMockData from '../mock_data/diff_discussions'; -import diffFileMockData from '../mock_data/diff_file'; - -const TEST_USER_ID = 'abc123'; -const TEST_USER = { id: TEST_USER_ID }; - -describe('InlineDiffTableRow', () => { - let wrapper; - let store; - const mockDiffContent = { - diffFile: diffFileMockData, - shouldRenderDraftRow: jest.fn(), - hasParallelDraftLeft: jest.fn(), - hasParallelDraftRight: jest.fn(), - draftForLine: jest.fn(), - }; - - const applyMap = mapInline(mockDiffContent); - const thisLine = applyMap(diffFileMockData.highlighted_diff_lines[0]); - - const createComponent = (props = {}, propsStore = store) => { - wrapper = shallowMount(InlineDiffTableRow, { - store: propsStore, - propsData: { - line: thisLine, - fileHash: diffFileMockData.file_hash, - filePath: diffFileMockData.file_path, - contextLinesPath: 'contextLinesPath', - isHighlighted: false, - ...props, - }, - }); - }; - - beforeEach(() => { - store = createStore(); - store.state.notes.userData = TEST_USER; - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('does not add hll class to line content when line does not match highlighted row', () => { - createComponent(); - expect(wrapper.find('.line_content').classes('hll')).toBe(false); - }); - - it('adds hll class to lineContent when line is the highlighted row', () => { - store.state.diffs.highlightedRow = thisLine.line_code; - createComponent({}, store); - expect(wrapper.find('.line_content').classes('hll')).toBe(true); - }); - - it('adds hll class to lineContent when line is part of a multiline comment', () => { - createComponent({ isCommented: true }); - expect(wrapper.find('.line_content').classes('hll')).toBe(true); - }); - - describe('sets coverage title and class', () => { - it('for lines with coverage', () => { - const name = diffFileMockData.file_path; - const line = thisLine.new_line; - - store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } }; - createComponent({}, store); - const coverage = wrapper.find('.line-coverage'); - - expect(coverage.attributes('title')).toContain('Test coverage: 5 hits'); - expect(coverage.classes('coverage')).toBe(true); - }); - - it('for lines without coverage', () => { - const name = diffFileMockData.file_path; - const line = thisLine.new_line; - - store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } }; - createComponent({}, store); - const coverage = wrapper.find('.line-coverage'); - - expect(coverage.attributes('title')).toContain('No test coverage'); - expect(coverage.classes('no-coverage')).toBe(true); - }); - - it('for unknown lines', () => { - store.state.diffs.coverageFiles = {}; - createComponent({}, store); - - const coverage = wrapper.find('.line-coverage'); - - expect(coverage.attributes('title')).toBeUndefined(); - expect(coverage.classes('coverage')).toBe(false); - expect(coverage.classes('no-coverage')).toBe(false); - }); - }); - - describe('Table Cells', () => { - const findNewTd = () => wrapper.find({ ref: 'newTd' }); - const findOldTd = () => wrapper.find({ ref: 'oldTd' }); - - describe('td', () => { - it('highlights when isHighlighted true', () => { - store.state.diffs.highlightedRow = thisLine.line_code; - createComponent({}, store); - - expect(findNewTd().classes()).toContain('hll'); - expect(findOldTd().classes()).toContain('hll'); - }); - - it('does not highlight when isHighlighted false', () => { - createComponent(); - - expect(findNewTd().classes()).not.toContain('hll'); - expect(findOldTd().classes()).not.toContain('hll'); - }); - }); - - describe('comment button', () => { - const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' }); - - it.each` - userData | expectation - ${TEST_USER} | ${true} - ${null} | ${false} - `('exists is $expectation - with userData ($userData)', ({ userData, expectation }) => { - store.state.notes.userData = userData; - createComponent({}, store); - - expect(findNoteButton().exists()).toBe(expectation); - }); - - it.each` - isHover | line | expectation - ${true} | ${{ ...thisLine, discussions: [] }} | ${true} - ${false} | ${{ ...thisLine, discussions: [] }} | ${false} - ${true} | ${{ ...thisLine, type: 'context', discussions: [] }} | ${false} - ${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false} - ${true} | ${{ ...thisLine, discussions: [{}] }} | ${false} - `('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => { - createComponent({ line: applyMap(line) }); - wrapper.setData({ isHover }); - - return wrapper.vm.$nextTick().then(() => { - expect(findNoteButton().isVisible()).toBe(expectation); - }); - }); - - it.each` - disabled | commentsDisabled - ${'disabled'} | ${true} - ${undefined} | ${false} - `( - 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled', - ({ disabled, commentsDisabled }) => { - createComponent({ - line: applyMap({ ...thisLine, commentsDisabled }), - }); - - wrapper.setData({ isHover: true }); - - return wrapper.vm.$nextTick().then(() => { - expect(findNoteButton().attributes('disabled')).toBe(disabled); - }); - }, - ); - - const symlinkishFileTooltip = - 'Commenting on symbolic links that replace or are replaced by files is currently not supported.'; - const realishFileTooltip = - 'Commenting on files that replace or are replaced by symbolic links is currently not supported.'; - const otherFileTooltip = 'Add a comment to this line'; - const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' }); - - it.each` - tooltip | commentsDisabled - ${symlinkishFileTooltip} | ${{ wasSymbolic: true }} - ${symlinkishFileTooltip} | ${{ isSymbolic: true }} - ${realishFileTooltip} | ${{ wasReal: true }} - ${realishFileTooltip} | ${{ isReal: true }} - ${otherFileTooltip} | ${false} - `( - 'has the correct tooltip when commentsDisabled=$commentsDisabled', - ({ tooltip, commentsDisabled }) => { - createComponent({ - line: applyMap({ ...thisLine, commentsDisabled }), - }); - - wrapper.setData({ isHover: true }); - - return wrapper.vm.$nextTick().then(() => { - expect(findTooltip().attributes('title')).toBe(tooltip); - }); - }, - ); - }); - - describe('line number', () => { - const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' }); - const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' }); - - it('renders line numbers in correct cells', () => { - createComponent(); - - expect(findLineNumberOld().exists()).toBe(false); - expect(findLineNumberNew().exists()).toBe(true); - }); - - describe('with lineNumber prop', () => { - const TEST_LINE_CODE = 'LC_42'; - const TEST_LINE_NUMBER = 1; - - describe.each` - lineProps | findLineNumber | expectedHref | expectedClickArg - ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE} - ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined} - ${{ line_code: undefined, left: { line_code: TEST_LINE_CODE }, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${TEST_LINE_CODE} - ${{ line_code: undefined, right: { line_code: TEST_LINE_CODE }, new_line: TEST_LINE_NUMBER }} | ${findLineNumberNew} | ${'#'} | ${TEST_LINE_CODE} - `( - 'with line ($lineProps)', - ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => { - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockImplementation(); - createComponent({ - line: applyMap({ ...thisLine, ...lineProps }), - }); - }); - - it('renders', () => { - expect(findLineNumber().exists()).toBe(true); - expect(findLineNumber().attributes()).toEqual({ - href: expectedHref, - 'data-linenumber': TEST_LINE_NUMBER.toString(), - }); - }); - - it('on click, dispatches setHighlightedRow', () => { - expect(store.dispatch).toHaveBeenCalledTimes(1); - - findLineNumber().trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith( - 'diffs/setHighlightedRow', - expectedClickArg, - ); - expect(store.dispatch).toHaveBeenCalledTimes(2); - }); - }, - ); - }); - }); - - describe('diff-gutter-avatars', () => { - const TEST_LINE_CODE = 'LC_42'; - const TEST_FILE_HASH = diffFileMockData.file_hash; - const findAvatars = () => wrapper.find(DiffGutterAvatars); - let line; - - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockImplementation(); - - line = { - line_code: TEST_LINE_CODE, - type: 'new', - old_line: null, - new_line: 1, - discussions: [{ ...discussionsMockData }], - discussionsExpanded: true, - text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', - rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', - meta_data: null, - }; - }); - - describe('with showCommentButton', () => { - it('renders if line has discussions', () => { - createComponent({ line: applyMap(line) }); - - expect(findAvatars().props()).toEqual({ - discussions: line.discussions, - discussionsExpanded: line.discussionsExpanded, - }); - }); - - it('does notrender if line has no discussions', () => { - line.discussions = []; - createComponent({ line: applyMap(line) }); - - expect(findAvatars().exists()).toEqual(false); - }); - - it('toggles line discussion', () => { - createComponent({ line: applyMap(line) }); - - expect(store.dispatch).toHaveBeenCalledTimes(1); - - findAvatars().vm.$emit('toggleLineDiscussions'); - - expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', { - lineCode: TEST_LINE_CODE, - fileHash: TEST_FILE_HASH, - expanded: !line.discussionsExpanded, - }); - }); - }); - }); - }); - - describe('interoperability', () => { - it.each` - desc | line | expectation - ${'with type old'} | ${{ ...thisLine, type: 'old', old_line: 3, new_line: 5 }} | ${{ type: 'old', line: '3', oldLine: '3', newLine: '5' }} - ${'with type new'} | ${{ ...thisLine, type: 'new', old_line: 3, new_line: 5 }} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }} - `('$desc, sets interop data attributes', ({ line, expectation }) => { - createComponent({ line }); - - expect(findInteropAttributes(wrapper)).toEqual(expectation); - }); - }); -}); diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js deleted file mode 100644 index 27834804f77..00000000000 --- a/spec/frontend/diffs/components/inline_diff_view_spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import '~/behaviors/markdown/render_gfm'; -import { getByText } from '@testing-library/dom'; -import { mount } from '@vue/test-utils'; -import { mapInline } from '~/diffs/components/diff_row_utils'; -import InlineDiffView from '~/diffs/components/inline_diff_view.vue'; -import { createStore } from '~/mr_notes/stores'; -import discussionsMockData from '../mock_data/diff_discussions'; -import diffFileMockData from '../mock_data/diff_file'; - -describe('InlineDiffView', () => { - let wrapper; - const getDiffFileMock = () => ({ ...diffFileMockData }); - const getDiscussionsMockData = () => [{ ...discussionsMockData }]; - const notesLength = getDiscussionsMockData()[0].notes.length; - - const setup = (diffFile, diffLines) => { - const mockDiffContent = { - diffFile, - shouldRenderDraftRow: jest.fn(), - }; - - const store = createStore(); - - store.dispatch('diffs/setInlineDiffViewType'); - wrapper = mount(InlineDiffView, { - store, - propsData: { - diffFile, - diffLines: diffLines.map(mapInline(mockDiffContent)), - }, - }); - }; - - describe('template', () => { - it('should have rendered diff lines', () => { - const diffFile = getDiffFileMock(); - setup(diffFile, diffFile.highlighted_diff_lines); - - expect(wrapper.findAll('tr.line_holder').length).toEqual(8); - expect(wrapper.findAll('tr.line_holder.new').length).toEqual(4); - expect(wrapper.findAll('tr.line_expansion.match').length).toEqual(1); - getByText(wrapper.element, /Bad dates/i); - }); - - it('should render discussions', () => { - const diffFile = getDiffFileMock(); - diffFile.highlighted_diff_lines[1].discussions = getDiscussionsMockData(); - diffFile.highlighted_diff_lines[1].discussionsExpanded = true; - setup(diffFile, diffFile.highlighted_diff_lines); - - expect(wrapper.findAll('.notes_holder').length).toEqual(1); - expect(wrapper.findAll('.notes_holder .note').length).toEqual(notesLength + 1); - getByText(wrapper.element, 'comment 5'); - wrapper.vm.$store.dispatch('setInitialNotes', []); - }); - }); -}); diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js deleted file mode 100644 index ed191d849fd..00000000000 --- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js +++ /dev/null @@ -1,445 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; -import { mapParallel } from '~/diffs/components/diff_row_utils'; -import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; -import { createStore } from '~/mr_notes/stores'; -import { findInteropAttributes } from '../find_interop_attributes'; -import discussionsMockData from '../mock_data/diff_discussions'; -import diffFileMockData from '../mock_data/diff_file'; - -describe('ParallelDiffTableRow', () => { - const mockDiffContent = { - diffFile: diffFileMockData, - shouldRenderDraftRow: jest.fn(), - hasParallelDraftLeft: jest.fn(), - hasParallelDraftRight: jest.fn(), - draftForLine: jest.fn(), - }; - - const applyMap = mapParallel(mockDiffContent); - - describe('when one side is empty', () => { - let wrapper; - let vm; - const thisLine = diffFileMockData.parallel_diff_lines[0]; - const rightLine = diffFileMockData.parallel_diff_lines[0].right; - - beforeEach(() => { - wrapper = shallowMount(ParallelDiffTableRow, { - store: createStore(), - propsData: { - line: applyMap(thisLine), - fileHash: diffFileMockData.file_hash, - filePath: diffFileMockData.file_path, - contextLinesPath: 'contextLinesPath', - isHighlighted: false, - }, - }); - - vm = wrapper.vm; - }); - - it('does not highlight non empty line content when line does not match highlighted row', (done) => { - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); - }) - .then(done) - .catch(done.fail); - }); - - it('highlights nonempty line content when line is the highlighted row', (done) => { - vm.$nextTick() - .then(() => { - vm.$store.state.diffs.highlightedRow = rightLine.line_code; - - return vm.$nextTick(); - }) - .then(() => { - expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); - }) - .then(done) - .catch(done.fail); - }); - - it('highlights nonempty line content when line is part of a multiline comment', () => { - wrapper.setProps({ isCommented: true }); - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); - }); - }); - }); - - describe('when both sides have content', () => { - let vm; - const thisLine = diffFileMockData.parallel_diff_lines[2]; - const rightLine = diffFileMockData.parallel_diff_lines[2].right; - - beforeEach(() => { - vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), { - line: applyMap(thisLine), - fileHash: diffFileMockData.file_hash, - filePath: diffFileMockData.file_path, - contextLinesPath: 'contextLinesPath', - isHighlighted: false, - }).$mount(); - }); - - it('does not highlight either line when line does not match highlighted row', (done) => { - vm.$nextTick() - .then(() => { - expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll'); - expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll'); - }) - .then(done) - .catch(done.fail); - }); - - it('adds hll class to lineContent when line is the highlighted row', (done) => { - vm.$nextTick() - .then(() => { - vm.$store.state.diffs.highlightedRow = rightLine.line_code; - - return vm.$nextTick(); - }) - .then(() => { - expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll'); - expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll'); - }) - .then(done) - .catch(done.fail); - }); - - describe('sets coverage title and class', () => { - it('for lines with coverage', (done) => { - vm.$nextTick() - .then(() => { - const name = diffFileMockData.file_path; - const line = rightLine.new_line; - - vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } }; - - return vm.$nextTick(); - }) - .then(() => { - const coverage = vm.$el.querySelector('.line-coverage.right-side'); - - expect(coverage.title).toContain('Test coverage: 5 hits'); - expect(coverage.classList).toContain('coverage'); - }) - .then(done) - .catch(done.fail); - }); - - it('for lines without coverage', (done) => { - vm.$nextTick() - .then(() => { - const name = diffFileMockData.file_path; - const line = rightLine.new_line; - - vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } }; - - return vm.$nextTick(); - }) - .then(() => { - const coverage = vm.$el.querySelector('.line-coverage.right-side'); - - expect(coverage.title).toContain('No test coverage'); - expect(coverage.classList).toContain('no-coverage'); - }) - .then(done) - .catch(done.fail); - }); - - it('for unknown lines', (done) => { - vm.$nextTick() - .then(() => { - vm.$store.state.diffs.coverageFiles = {}; - - return vm.$nextTick(); - }) - .then(() => { - const coverage = vm.$el.querySelector('.line-coverage.right-side'); - - expect(coverage.title).not.toContain('Coverage'); - expect(coverage.classList).not.toContain('coverage'); - expect(coverage.classList).not.toContain('no-coverage'); - }) - .then(done) - .catch(done.fail); - }); - }); - }); - - describe('Table Cells', () => { - let wrapper; - let store; - let thisLine; - const TEST_USER_ID = 'abc123'; - const TEST_USER = { id: TEST_USER_ID }; - - const createComponent = (props = {}, propsStore = store, data = {}) => { - wrapper = shallowMount(ParallelDiffTableRow, { - store: propsStore, - propsData: { - line: thisLine, - fileHash: diffFileMockData.file_hash, - filePath: diffFileMockData.file_path, - contextLinesPath: 'contextLinesPath', - isHighlighted: false, - ...props, - }, - data() { - return data; - }, - }); - }; - - beforeEach(() => { - // eslint-disable-next-line prefer-destructuring - thisLine = diffFileMockData.parallel_diff_lines[2]; - store = createStore(); - store.state.notes.userData = TEST_USER; - }); - - afterEach(() => { - wrapper.destroy(); - }); - - const findNewTd = () => wrapper.find({ ref: 'newTd' }); - const findOldTd = () => wrapper.find({ ref: 'oldTd' }); - - describe('td', () => { - it('highlights when isHighlighted true', () => { - store.state.diffs.highlightedRow = thisLine.left.line_code; - createComponent({}, store); - - expect(findNewTd().classes()).toContain('hll'); - expect(findOldTd().classes()).toContain('hll'); - }); - - it('does not highlight when isHighlighted false', () => { - createComponent(); - - expect(findNewTd().classes()).not.toContain('hll'); - expect(findOldTd().classes()).not.toContain('hll'); - }); - }); - - describe('comment button', () => { - const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' }); - - it.each` - hover | line | userData | expectation - ${true} | ${{}} | ${TEST_USER} | ${true} - ${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${false} - ${true} | ${{}} | ${null} | ${false} - ${false} | ${{}} | ${TEST_USER} | ${false} - `( - 'exists is $expectation - with userData ($userData)', - async ({ hover, line, userData, expectation }) => { - store.state.notes.userData = userData; - createComponent(line, store); - if (hover) await wrapper.find('.line_holder').trigger('mouseover'); - - expect(findNoteButton().exists()).toBe(expectation); - }, - ); - - it.each` - line | expectation - ${{ ...thisLine, left: { discussions: [] } }} | ${true} - ${{ ...thisLine, left: { type: 'context', discussions: [] } }} | ${false} - ${{ ...thisLine, left: { type: 'old-nonewline', discussions: [] } }} | ${false} - ${{ ...thisLine, left: { discussions: [{}] } }} | ${false} - `('visible is $expectation - line ($line)', async ({ line, expectation }) => { - createComponent({ line: applyMap(line) }, store, { - isLeftHover: true, - isCommentButtonRendered: true, - }); - - expect(findNoteButton().isVisible()).toBe(expectation); - }); - - it.each` - disabled | commentsDisabled - ${'disabled'} | ${true} - ${undefined} | ${false} - `( - 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled', - ({ disabled, commentsDisabled }) => { - thisLine.left.commentsDisabled = commentsDisabled; - createComponent({ line: { ...thisLine } }, store, { - isLeftHover: true, - isCommentButtonRendered: true, - }); - - expect(findNoteButton().attributes('disabled')).toBe(disabled); - }, - ); - - const symlinkishFileTooltip = - 'Commenting on symbolic links that replace or are replaced by files is currently not supported.'; - const realishFileTooltip = - 'Commenting on files that replace or are replaced by symbolic links is currently not supported.'; - const otherFileTooltip = 'Add a comment to this line'; - const findTooltip = () => wrapper.find({ ref: 'addNoteTooltipLeft' }); - - it.each` - tooltip | commentsDisabled - ${symlinkishFileTooltip} | ${{ wasSymbolic: true }} - ${symlinkishFileTooltip} | ${{ isSymbolic: true }} - ${realishFileTooltip} | ${{ wasReal: true }} - ${realishFileTooltip} | ${{ isReal: true }} - ${otherFileTooltip} | ${false} - `( - 'has the correct tooltip when commentsDisabled=$commentsDisabled', - ({ tooltip, commentsDisabled }) => { - thisLine.left.commentsDisabled = commentsDisabled; - createComponent({ line: { ...thisLine } }, store, { - isLeftHover: true, - isCommentButtonRendered: true, - }); - - expect(findTooltip().attributes('title')).toBe(tooltip); - }, - ); - }); - - describe('line number', () => { - const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' }); - const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' }); - - it('renders line numbers in correct cells', () => { - createComponent(); - - expect(findLineNumberOld().exists()).toBe(true); - expect(findLineNumberNew().exists()).toBe(true); - }); - - describe('with lineNumber prop', () => { - const TEST_LINE_CODE = 'LC_42'; - const TEST_LINE_NUMBER = 1; - - describe.each` - lineProps | findLineNumber | expectedHref | expectedClickArg - ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE} - ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined} - `( - 'with line ($lineProps)', - ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => { - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockImplementation(); - Object.assign(thisLine.left, lineProps); - Object.assign(thisLine.right, lineProps); - createComponent({ - line: applyMap({ ...thisLine }), - }); - }); - - it('renders', () => { - expect(findLineNumber().exists()).toBe(true); - expect(findLineNumber().attributes()).toEqual({ - href: expectedHref, - 'data-linenumber': TEST_LINE_NUMBER.toString(), - }); - }); - - it('on click, dispatches setHighlightedRow', () => { - expect(store.dispatch).toHaveBeenCalledTimes(1); - - findLineNumber().trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith( - 'diffs/setHighlightedRow', - expectedClickArg, - ); - expect(store.dispatch).toHaveBeenCalledTimes(2); - }); - }, - ); - }); - }); - - describe('diff-gutter-avatars', () => { - const TEST_LINE_CODE = 'LC_42'; - const TEST_FILE_HASH = diffFileMockData.file_hash; - const findAvatars = () => wrapper.find(DiffGutterAvatars); - let line; - - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockImplementation(); - - line = applyMap({ - left: { - line_code: TEST_LINE_CODE, - type: 'new', - old_line: null, - new_line: 1, - discussions: [{ ...discussionsMockData }], - discussionsExpanded: true, - text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', - rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', - meta_data: null, - }, - }); - }); - - describe('with showCommentButton', () => { - it('renders if line has discussions', () => { - createComponent({ line }); - - expect(findAvatars().props()).toEqual({ - discussions: line.left.discussions, - discussionsExpanded: line.left.discussionsExpanded, - }); - }); - - it('does notrender if line has no discussions', () => { - line.left.discussions = []; - createComponent({ line: applyMap(line) }); - - expect(findAvatars().exists()).toEqual(false); - }); - - it('toggles line discussion', () => { - createComponent({ line }); - - expect(store.dispatch).toHaveBeenCalledTimes(1); - - findAvatars().vm.$emit('toggleLineDiscussions'); - - expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', { - lineCode: TEST_LINE_CODE, - fileHash: TEST_FILE_HASH, - expanded: !line.left.discussionsExpanded, - }); - }); - }); - }); - - describe('interoperability', () => { - beforeEach(() => { - createComponent(); - }); - - it('adds old side interoperability data attributes', () => { - expect(findInteropAttributes(wrapper, '.line_content.left-side')).toEqual({ - type: 'old', - line: thisLine.left.old_line.toString(), - oldLine: thisLine.left.old_line.toString(), - }); - }); - - it('adds new side interoperability data attributes', () => { - expect(findInteropAttributes(wrapper, '.line_content.right-side')).toEqual({ - type: 'new', - line: thisLine.right.new_line.toString(), - newLine: thisLine.right.new_line.toString(), - }); - }); - }); - }); -}); diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js deleted file mode 100644 index 452e1f58551..00000000000 --- a/spec/frontend/diffs/components/parallel_diff_view_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import Vuex from 'vuex'; -import parallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; -import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue'; -import { createStore } from '~/mr_notes/stores'; -import diffFileMockData from '../mock_data/diff_file'; - -let wrapper; -const localVue = createLocalVue(); - -localVue.use(Vuex); - -function factory() { - const diffFile = { ...diffFileMockData }; - const store = createStore(); - - wrapper = shallowMount(ParallelDiffView, { - localVue, - store, - propsData: { - diffFile, - diffLines: diffFile.parallel_diff_lines, - }, - }); -} - -describe('ParallelDiffView', () => { - afterEach(() => { - wrapper.destroy(); - }); - - it('renders diff lines', () => { - factory(); - - expect(wrapper.findAll(parallelDiffTableRow).length).toBe(8); - }); -}); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js index 93cddff8421..1b97011bf7f 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js @@ -11,7 +11,7 @@ import { processFilters, filterToQueryObject, urlQueryToFilter, - getRecentlyUsedTokenValues, + getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed, } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; @@ -328,32 +328,32 @@ describe('urlQueryToFilter', () => { ); }); -describe('getRecentlyUsedTokenValues', () => { +describe('getRecentlyUsedSuggestions', () => { useLocalStorageSpy(); beforeEach(() => { localStorage.removeItem(mockStorageKey); }); - it('returns array containing recently used token values from provided recentTokenValuesStorageKey', () => { + it('returns array containing recently used token values from provided recentSuggestionsStorageKey', () => { setLocalStorageAvailability(true); const mockExpectedArray = [{ foo: 'bar' }]; localStorage.setItem(mockStorageKey, JSON.stringify(mockExpectedArray)); - expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual(mockExpectedArray); + expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual(mockExpectedArray); }); - it('returns empty array when provided recentTokenValuesStorageKey does not have anything in localStorage', () => { + it('returns empty array when provided recentSuggestionsStorageKey does not have anything in localStorage', () => { setLocalStorageAvailability(true); - expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]); + expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual([]); }); it('returns empty array when when access to localStorage is not available', () => { setLocalStorageAvailability(false); - expect(getRecentlyUsedTokenValues(mockStorageKey)).toEqual([]); + expect(getRecentlyUsedSuggestions(mockStorageKey)).toEqual([]); }); }); @@ -366,7 +366,7 @@ describe('setTokenValueToRecentlyUsed', () => { localStorage.removeItem(mockStorageKey); }); - it('adds provided tokenValue to localStorage for recentTokenValuesStorageKey', () => { + it('adds provided tokenValue to localStorage for recentSuggestionsStorageKey', () => { setLocalStorageAvailability(true); setTokenValueToRecentlyUsed(mockStorageKey, mockTokenValue1); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 951b050495c..74f579e77ed 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -94,7 +94,7 @@ describe('AuthorToken', () => { it('calls `config.fetchAuthors` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchAuthors'); - getBaseToken().vm.$emit('fetch-token-values', mockAuthors[0].username); + getBaseToken().vm.$emit('fetch-suggestions', mockAuthors[0].username); expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith( mockAuthorToken.fetchPath, @@ -105,17 +105,17 @@ describe('AuthorToken', () => { it('sets response to `authors` when request is succesful', () => { jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors); - getBaseToken().vm.$emit('fetch-token-values', 'root'); + getBaseToken().vm.$emit('fetch-suggestions', 'root'); return waitForPromises().then(() => { - expect(getBaseToken().props('tokenValues')).toEqual(mockAuthors); + expect(getBaseToken().props('suggestions')).toEqual(mockAuthors); }); }); it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({}); - getBaseToken().vm.$emit('fetch-token-values', 'root'); + getBaseToken().vm.$emit('fetch-suggestions', 'root'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -127,17 +127,17 @@ describe('AuthorToken', () => { it('sets `loading` to false when request completes', async () => { jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({}); - getBaseToken().vm.$emit('fetch-token-values', 'root'); + getBaseToken().vm.$emit('fetch-suggestions', 'root'); await waitForPromises(); - expect(getBaseToken().props('tokensListLoading')).toBe(false); + expect(getBaseToken().props('suggestionsLoading')).toBe(false); }); }); }); describe('template', () => { - const activateTokenValuesList = async () => { + const activateSuggestionsList = async () => { const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); const suggestionsSegment = tokenSegments.at(2); suggestionsSegment.vm.$emit('activate'); @@ -154,7 +154,7 @@ describe('AuthorToken', () => { expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ - tokenValues: mockAuthors, + suggestions: mockAuthors, fnActiveTokenValue: wrapper.vm.getActiveAuthor, }); }); @@ -221,7 +221,7 @@ describe('AuthorToken', () => { stubs: { Portal: true }, }); - await activateTokenValuesList(); + await activateSuggestionsList(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -252,7 +252,7 @@ describe('AuthorToken', () => { stubs: { Portal: true }, }); - await activateTokenValuesList(); + await activateSuggestionsList(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index 89c5cedc9b8..cd6ffd679d0 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -7,7 +7,7 @@ import { import { DEFAULT_LABELS } from '~/vue_shared/components/filtered_search_bar/constants'; import { - getRecentlyUsedTokenValues, + getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed, } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -49,10 +49,10 @@ const mockProps = { config: mockLabelToken, value: { data: '' }, active: false, - tokenValues: [], - tokensListLoading: false, - defaultTokenValues: DEFAULT_LABELS, - recentTokenValuesStorageKey: mockStorageKey, + suggestions: [], + suggestionsLoading: false, + defaultSuggestions: DEFAULT_LABELS, + recentSuggestionsStorageKey: mockStorageKey, fnCurrentTokenValue: jest.fn(), }; @@ -83,7 +83,7 @@ describe('BaseToken', () => { props: { ...mockProps, value: { data: `"${mockRegularLabel.title}"` }, - tokenValues: mockLabels, + suggestions: mockLabels, }, }); }); @@ -93,8 +93,8 @@ describe('BaseToken', () => { }); describe('data', () => { - it('calls `getRecentlyUsedTokenValues` to populate `recentTokenValues` when `recentTokenValuesStorageKey` is defined', () => { - expect(getRecentlyUsedTokenValues).toHaveBeenCalledWith(mockStorageKey); + it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => { + expect(getRecentlyUsedSuggestions).toHaveBeenCalledWith(mockStorageKey); }); }); @@ -147,15 +147,15 @@ describe('BaseToken', () => { wrapperWithTokenActive.destroy(); }); - it('emits `fetch-token-values` event on the component when value of this prop is changed to false and `tokenValues` array is empty', async () => { + it('emits `fetch-suggestions` event on the component when value of this prop is changed to false and `suggestions` array is empty', async () => { wrapperWithTokenActive.setProps({ active: false, }); await wrapperWithTokenActive.vm.$nextTick(); - expect(wrapperWithTokenActive.emitted('fetch-token-values')).toBeTruthy(); - expect(wrapperWithTokenActive.emitted('fetch-token-values')).toEqual([ + expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toBeTruthy(); + expect(wrapperWithTokenActive.emitted('fetch-suggestions')).toEqual([ [`"${mockRegularLabel.title}"`], ]); }); @@ -164,7 +164,7 @@ describe('BaseToken', () => { describe('methods', () => { describe('handleTokenValueSelected', () => { - it('calls `setTokenValueToRecentlyUsed` when `recentTokenValuesStorageKey` is defined', () => { + it('calls `setTokenValueToRecentlyUsed` when `recentSuggestionsStorageKey` is defined', () => { const mockTokenValue = { id: 1, title: 'Foo', @@ -175,14 +175,14 @@ describe('BaseToken', () => { expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); }); - it('does not add token from preloadedTokenValues', async () => { + it('does not add token from preloadedSuggestions', async () => { const mockTokenValue = { id: 1, title: 'Foo', }; wrapper.setProps({ - preloadedTokenValues: [mockTokenValue], + preloadedSuggestions: [mockTokenValue], }); await wrapper.vm.$nextTick(); @@ -228,7 +228,7 @@ describe('BaseToken', () => { wrapperWithNoStubs.destroy(); }); - it('emits `fetch-token-values` event on component after a delay when component emits `input` event', async () => { + it('emits `fetch-suggestions` event on component after a delay when component emits `input` event', async () => { jest.useFakeTimers(); wrapperWithNoStubs.find(GlFilteredSearchToken).vm.$emit('input', { data: 'foo' }); @@ -236,8 +236,8 @@ describe('BaseToken', () => { jest.runAllTimers(); - expect(wrapperWithNoStubs.emitted('fetch-token-values')).toBeTruthy(); - expect(wrapperWithNoStubs.emitted('fetch-token-values')[2]).toEqual(['foo']); + expect(wrapperWithNoStubs.emitted('fetch-suggestions')).toBeTruthy(); + expect(wrapperWithNoStubs.emitted('fetch-suggestions')[2]).toEqual(['foo']); }); }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js index cc40ff96b65..ec9458f64d2 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js @@ -159,7 +159,7 @@ describe('LabelToken', () => { expect(baseTokenEl.exists()).toBe(true); expect(baseTokenEl.props()).toMatchObject({ - tokenValues: mockLabels, + suggestions: mockLabels, fnActiveTokenValue: wrapper.vm.getActiveLabel, }); }); diff --git a/spec/frontend_integration/diffs/diffs_interopability_spec.js b/spec/frontend_integration/diffs/diffs_interopability_spec.js index cb7659e16d3..448641ed834 100644 --- a/spec/frontend_integration/diffs/diffs_interopability_spec.js +++ b/spec/frontend_integration/diffs/diffs_interopability_spec.js @@ -8,15 +8,6 @@ import { getCodeElementFromLineNumber, } from './diffs_interopability_api'; -jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({ - inject: { - glFeatures: { - from: 'window.gon.features', - default: () => global.window.gon?.features, - }, - }, -})); - const TEST_PROJECT_PATH = 'gitlab-org/gitlab-test'; const TEST_BASE_URL = `/${TEST_PROJECT_PATH}/-/merge_requests/1/`; const TEST_DIFF_FILE = 'files/js/commit.coffee'; @@ -114,48 +105,41 @@ describe('diffs third party interoperability', () => { ); describe.each` - desc | unifiedDiffComponents | view | rowSelector | codeSelector | expectation - ${'inline view'} | ${false} | ${'inline'} | ${'tr.line_holder'} | ${'td.line_content'} | ${EXPECT_INLINE} - ${'parallel view left side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE} - ${'parallel view right side'} | ${false} | ${'parallel'} | ${'tr.line_holder'} | ${'td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE} - ${'inline view'} | ${true} | ${'inline'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content'} | ${EXPECT_INLINE} - ${'parallel view left side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE} - ${'parallel view right side'} | ${true} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE} - `( - '$desc (unifiedDiffComponents=$unifiedDiffComponents)', - ({ unifiedDiffComponents, view, rowSelector, codeSelector, expectation }) => { - beforeEach(async () => { - global.jsdom.reconfigure({ - url: `${TEST_HOST}/${TEST_BASE_URL}/diffs?view=${view}`, - }); - window.gon.features = { unifiedDiffComponents }; - - vm = startDiffsApp(); - - await waitFor(() => expect(hasLines(rowSelector)).toBe(true)); + desc | view | rowSelector | codeSelector | expectation + ${'inline view'} | ${'inline'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content'} | ${EXPECT_INLINE} + ${'parallel view left side'} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.left-side'} | ${EXPECT_PARALLEL_LEFT_SIDE} + ${'parallel view right side'} | ${'parallel'} | ${'.diff-tr.line_holder'} | ${'.diff-td.line_content.right-side'} | ${EXPECT_PARALLEL_RIGHT_SIDE} + `('$desc', ({ view, rowSelector, codeSelector, expectation }) => { + beforeEach(async () => { + global.jsdom.reconfigure({ + url: `${TEST_HOST}/${TEST_BASE_URL}/diffs?view=${view}`, }); - it('should match diff model', () => { - const lines = findLineElements(rowSelector); - const codes = findCodeElements(lines, codeSelector); + vm = startDiffsApp(); - expect(getCodeElementsInteropModel(codes)).toEqual(expectation); - }); + await waitFor(() => expect(hasLines(rowSelector)).toBe(true)); + }); + + it('should match diff model', () => { + const lines = findLineElements(rowSelector); + const codes = findCodeElements(lines, codeSelector); + + expect(getCodeElementsInteropModel(codes)).toEqual(expectation); + }); - it.each` - lineNumber | part | expectedText - ${4} | ${'base'} | ${'new CommitFile(this)'} - ${4} | ${'head'} | ${'new CommitFile(@)'} - ${2} | ${'base'} | ${'constructor: ->'} - ${2} | ${'head'} | ${'constructor: ->'} - `( - 'should find code element lineNumber=$lineNumber part=$part', - ({ lineNumber, part, expectedText }) => { - const codeElement = getCodeElementFromLineNumber(findDiffFile(), lineNumber, part); - - expect(codeElement.textContent.trim()).toBe(expectedText); - }, - ); - }, - ); + it.each` + lineNumber | part | expectedText + ${4} | ${'base'} | ${'new CommitFile(this)'} + ${4} | ${'head'} | ${'new CommitFile(@)'} + ${2} | ${'base'} | ${'constructor: ->'} + ${2} | ${'head'} | ${'constructor: ->'} + `( + 'should find code element lineNumber=$lineNumber part=$part', + ({ lineNumber, part, expectedText }) => { + const codeElement = getCodeElementFromLineNumber(findDiffFile(), lineNumber, part); + + expect(codeElement.textContent.trim()).toBe(expectedText); + }, + ); + }); }); diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 8d625cab1d8..9bd89abae83 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -83,24 +83,8 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac expect(pipeline_status).not_to be_has_cache end - context 'ci_pipeline_status_omit_commit_sha_in_cache_key is enabled' do - before do - stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: project) - end - - it 'makes a Gitaly call' do - expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) - end - end - - context 'ci_pipeline_status_omit_commit_sha_in_cache_key is disabled' do - before do - stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: false) - end - - it 'makes a Gitaly calls' do - expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) - end + it 'makes a Gitaly call' do + expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) end end @@ -111,24 +95,8 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac expect(pipeline_status).to be_has_cache end - context 'ci_pipeline_status_omit_commit_sha_in_cache_key is enabled' do - before do - stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: project) - end - - it 'makes no Gitaly calls' do - expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(0) - end - end - - context 'ci_pipeline_status_omit_commit_sha_in_cache_key is disabled' do - before do - stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: false) - end - - it 'makes a Gitaly calls' do - expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) - end + it 'makes no Gitaly calls' do + expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(0) end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 62dec522161..9c118325421 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1966,6 +1966,23 @@ RSpec.describe Ci::Build do end end + describe '#tag_list' do + let_it_be(:build) { create(:ci_build, tag_list: ['tag']) } + + context 'when tags are preloaded' do + it 'does not trigger queries' do + build_with_tags = described_class.eager_load_tags.id_in([build]).to_a.first + + expect { build_with_tags.tag_list }.not_to exceed_all_query_limit(0) + expect(build_with_tags.tag_list).to eq(['tag']) + end + end + + context 'when tags are not preloaded' do + it { expect(described_class.find(build.id).tag_list).to eq(['tag']) } + end + end + describe '#has_tags?' do context 'when build has tags' do subject { create(:ci_build, tag_list: ['tag']) } diff --git a/spec/presenters/packages/nuget/service_index_presenter_spec.rb b/spec/presenters/packages/nuget/service_index_presenter_spec.rb index 9c95fbc8fd2..aa69a9c3017 100644 --- a/spec/presenters/packages/nuget/service_index_presenter_spec.rb +++ b/spec/presenters/packages/nuget/service_index_presenter_spec.rb @@ -27,7 +27,7 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do describe '#resources' do subject { presenter.resources } - shared_examples 'returning valid resources' do |resources_count: 8, include_publish_service: true| + shared_examples 'returning valid resources' do |resources_count: 9, include_publish_service: true| it 'has valid resources' do expect(subject.size).to eq resources_count subject.each do |resource| @@ -38,10 +38,15 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do end end - it "does #{'not ' unless include_publish_service}return the publish resource" do + it "does #{'not ' unless include_publish_service}return the publish resource", :aggregate_failures do services_types = subject.map { |res| res[:@type] } - described_class::SERVICE_VERSIONS[:publish].each do |publish_service_version| + publish_service_versions = [ + described_class::SERVICE_VERSIONS[:publish], + described_class::SERVICE_VERSIONS[:symbol] + ].flatten + + publish_service_versions.each do |publish_service_version| if include_publish_service expect(services_types).to include(publish_service_version) else @@ -54,7 +59,7 @@ RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do context 'for a group' do let(:target) { group } - # at the group level we don't have the publish and download service + # at the group level we don't have the publish, symbol, and download service it_behaves_like 'returning valid resources', resources_count: 6, include_publish_service: false end diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb index 572736cfc86..a9e695dacbb 100644 --- a/spec/requests/api/nuget_project_packages_spec.rb +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -92,9 +92,10 @@ RSpec.describe API::NugetProjectPackages do describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do let_it_be(:package_name) { 'Dummy.Package' } - let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } + let_it_be(:package) { create(:nuget_package, :with_symbol_package, project: project, name: package_name) } - let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" } + let(:format) { 'nupkg' } + let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.#{format}" } subject { get api(url) } @@ -154,50 +155,7 @@ RSpec.describe API::NugetProjectPackages do subject { put api(url), headers: headers } - context 'with valid project' do - where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success - 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - let(:headers) { user_headers.merge(workhorse_headers) } - - before do - update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end - - it_behaves_like 'deploy token for package uploads' - - it_behaves_like 'job token for package uploads', authorize_endpoint: true do - let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } - end - - it_behaves_like 'rejects nuget access with unknown target id' - - it_behaves_like 'rejects nuget access with invalid target id' + it_behaves_like 'nuget authorize upload endpoint' end describe 'PUT /api/v4/projects/:id/packages/nuget' do @@ -221,63 +179,42 @@ RSpec.describe API::NugetProjectPackages do ) end - context 'with valid project' do - where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created - 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden - 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized - end - - with_them do - let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } - let(:headers) { user_headers.merge(workhorse_headers) } - let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } } - - before do - update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) - end - - it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] - end - end + it_behaves_like 'nuget upload endpoint' + end - it_behaves_like 'deploy token for package uploads' + describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage/authorize' do + include_context 'workhorse headers' - it_behaves_like 'job token for package uploads' do - let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } - end + let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage/authorize" } + let(:headers) { {} } - it_behaves_like 'rejects nuget access with unknown target id' + subject { put api(url), headers: headers } - it_behaves_like 'rejects nuget access with invalid target id' + it_behaves_like 'nuget authorize upload endpoint' + end - context 'file size above maximum limit' do - let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + describe 'PUT /api/v4/projects/:id/packages/nuget/symbolpackage' do + include_context 'workhorse headers' - before do - allow_next_instance_of(UploadedFile) do |uploaded_file| - allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) - end - end + let_it_be(:file_name) { 'package.snupkg' } + let(:url) { "/projects/#{target.id}/packages/nuget/symbolpackage" } + let(:headers) { {} } + let(:params) { { package: temp_file(file_name) } } + let(:file_key) { :package } + let(:send_rewritten_field) { true } - it_behaves_like 'returning response status', :bad_request + subject do + workhorse_finalize( + api(url), + method: :put, + file_key: file_key, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) end + + it_behaves_like 'nuget upload endpoint', symbol_package: true end def update_visibility_to(visibility) diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb index 79428b58bd9..8eddd27f8a2 100644 --- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb +++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb @@ -21,7 +21,8 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do version: '12.0.3' } ], - package_tags: [] + package_tags: [], + package_types: [] } it { is_expected.to eq(expected_metadata) } @@ -47,6 +48,16 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do end end + context 'with package types' do + let(:nuspec_filepath) { 'packages/nuget/with_package_types.nuspec' } + + it { is_expected.to have_key(:package_types) } + + it 'extracts package types' do + expect(subject[:package_types]).to include('SymbolsPackage') + end + end + context 'with a nuspec file with metadata' do let(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' } diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb index ffe1a5b7646..328484c3e5a 100644 --- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb +++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_redis_shared_state do include ExclusiveLeaseHelpers - let(:package) { create(:nuget_package, :processing) } + let(:package) { create(:nuget_package, :processing, :with_symbol_package) } let(:package_file) { package.package_files.first } let(:service) { described_class.new(package_file) } let(:package_name) { 'DummyProject.DummyPackage' } @@ -201,6 +201,41 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_ it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError end + context 'with a symbol package' do + let(:package_file) { package.package_files.last } + let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.snupkg' } + + context 'with no existing package' do + let(:package_id) { package.id } + + it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError + end + + context 'with existing package' do + let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) } + let(:package_id) { existing_package.id } + + it 'link existing package and updates package file', :aggregate_failures do + expect(service).to receive(:try_obtain_lease).and_call_original + expect(::Packages::Nuget::SyncMetadatumService).not_to receive(:new) + expect(::Packages::UpdateTagsService).not_to receive(:new) + + expect { subject } + .to change { ::Packages::Package.count }.by(-1) + .and change { Packages::Dependency.count }.by(0) + .and change { Packages::DependencyLink.count }.by(0) + .and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0) + .and change { ::Packages::Nuget::Metadatum.count }.by(0) + expect(package_file.reload.file_name).to eq(package_file_name) + expect(package_file.package).to eq(existing_package) + end + + it_behaves_like 'taking the lease' + + it_behaves_like 'not updating the package if the lease is taken' + end + end + context 'with an invalid package name' do invalid_names = [ '', diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 492b9a478fe..a71fc172ebd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -271,7 +271,6 @@ RSpec.configure do |config| # See https://gitlab.com/gitlab-org/gitlab/-/issues/33867 stub_feature_flags(file_identifier_hash: false) - stub_feature_flags(unified_diff_components: false) stub_feature_flags(diffs_virtual_scrolling: false) # The following `vue_issues_list`/`vue_issuables_list` stubs can be removed diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb index 49beecc6d4b..30afde7efed 100644 --- a/spec/support/helpers/merge_request_diff_helpers.rb +++ b/spec/support/helpers/merge_request_diff_helpers.rb @@ -3,8 +3,8 @@ module MergeRequestDiffHelpers def click_diff_line(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) - line[:content].hover - line[:num].find('.js-add-diff-note-button', visible: false).send_keys(:return) + line_holder.hover + line[:num].find('.js-add-diff-note-button').click end def get_line_components(line_holder, diff_side = nil) diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 617fdecbb5b..cdbb8f8c6e8 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -136,8 +136,8 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta end end -RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true| - RSpec.shared_examples 'creates nuget package files' do +RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true, symbol_package = false| + shared_examples 'creates nuget package files' do it 'creates package files' do expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect { subject } @@ -146,7 +146,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = expect(response).to have_gitlab_http_status(status) package_file = target.packages.last.package_files.reload.last - expect(package_file.file_name).to eq('package.nupkg') + expect(package_file.file_name).to eq(file_name) end end @@ -169,7 +169,10 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creates nuget package files' - it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' + + unless symbol_package + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' + end end end @@ -300,6 +303,16 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end + context 'with symbol package' do + let(:format) { 'snupkg' } + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + end + context 'with lower case package name' do let_it_be(:package_name) { 'dummy.package' } @@ -407,3 +420,114 @@ RSpec.shared_examples 'rejects nuget access with unknown target id' do end end end + +RSpec.shared_examples 'nuget authorize upload endpoint' do + using RSpec::Parameterized::TableSyntax + + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + + before do + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'job token for package uploads', authorize_endpoint: true do + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + end + + it_behaves_like 'rejects nuget access with unknown target id' + + it_behaves_like 'rejects nuget access with invalid target id' +end + +RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| + using RSpec::Parameterized::TableSyntax + + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } } + + before do + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], symbol_package + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'job token for package uploads' do + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + end + + it_behaves_like 'rejects nuget access with unknown target id' + + it_behaves_like 'rejects nuget access with invalid target id' + + context 'file size above maximum limit' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) + end + end + + it_behaves_like 'returning response status', :bad_request + end +end |