diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-02 15:09:59 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-02 15:09:59 +0000 |
commit | 1c2ff01b694fd06be15bc20279eef71ee5adf402 (patch) | |
tree | 98a588172ab8021790538a515933cf83552c5086 /spec | |
parent | 2e4e6e9bb63212c628e67c6865fa39f62217a83d (diff) | |
download | gitlab-ce-1c2ff01b694fd06be15bc20279eef71ee5adf402.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
18 files changed, 445 insertions, 153 deletions
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 4c42800cf05..c002d199b01 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -55,7 +55,7 @@ RSpec.describe 'User uses header search field', :js do expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i) end - context 'when clicking issues' do + context 'when clicking issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332317' do let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } it 'shows assigned issues' do @@ -75,7 +75,7 @@ RSpec.describe 'User uses header search field', :js do end end - context 'when clicking merge requests' do + context 'when clicking merge requests', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332317' do let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } it 'shows assigned merge requests' do diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 0722dcde0a3..f708d8c7728 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -116,6 +116,24 @@ describe('Api', () => { }); }); }); + + describe('deleteProjectPackageFile', () => { + const packageFileId = 'package_file_id'; + + it('delete a package', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages/${packageId}/package_files/${packageFileId}`; + + jest.spyOn(axios, 'delete'); + mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, true); + + return Api.deleteProjectPackageFile(projectId, packageId, packageFileId).then( + ({ data }) => { + expect(data).toEqual(true); + expect(axios.delete).toHaveBeenCalledWith(expectedUrl); + }, + ); + }); + }); }); describe('container registry', () => { diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb index 134d29d3106..003f7b768dd 100644 --- a/spec/frontend/fixtures/startup_css.rb +++ b/spec/frontend/fixtures/startup_css.rb @@ -11,12 +11,13 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do before(:all) do stub_feature_flags(combined_menu: true) + stub_feature_flags(sidebar_refactor: true) clean_frontend_fixtures('startup_css/') end shared_examples 'startup css project fixtures' do |type| let(:user) { create(:user, :admin) } - let(:project) { create(:project, :public, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png'), creator: user) } + let(:project) { create(:project, :public, :repository, description: 'Code and stuff', creator: user) } before do sign_in(user) @@ -42,6 +43,17 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do expect(response).to be_successful end + it "startup_css/project-#{type}-legacy-sidebar.html" do + stub_feature_flags(sidebar_refactor: false) + + get :show, params: { + namespace_id: project.namespace.to_param, + id: project + } + + expect(response).to be_successful + end + it "startup_css/project-#{type}-signed-out.html" do sign_out(user) diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js index 11dad7ba34d..4b890f868f4 100644 --- a/spec/frontend/packages/details/components/app_spec.js +++ b/spec/frontend/packages/details/components/app_spec.js @@ -1,4 +1,4 @@ -import { GlEmptyState, GlModal } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import stubChildren from 'helpers/stub_children'; @@ -34,6 +34,7 @@ describe('PackagesApp', () => { let store; const fetchPackageVersions = jest.fn(); const deletePackage = jest.fn(); + const deletePackageFile = jest.fn(); const defaultProjectName = 'bar'; const { location } = window; @@ -59,6 +60,7 @@ describe('PackagesApp', () => { actions: { deletePackage, fetchPackageVersions, + deletePackageFile, }, getters, }); @@ -82,8 +84,8 @@ describe('PackagesApp', () => { const packageTitle = () => wrapper.find(PackageTitle); const emptyState = () => wrapper.find(GlEmptyState); const deleteButton = () => wrapper.find('.js-delete-button'); - const deleteModal = () => wrapper.find(GlModal); - const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' }); + const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' }); + const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' }); const versionsTab = () => wrapper.find('.js-versions-tab > a'); const packagesLoader = () => wrapper.find(PackagesListLoader); const packagesVersionRows = () => wrapper.findAll(PackageListRow); @@ -110,7 +112,7 @@ describe('PackagesApp', () => { it('renders the app and displays the package title', () => { createComponent(); - expect(packageTitle()).toExist(); + expect(packageTitle().exists()).toBe(true); }); it('renders an empty state component when no an invalid package is passed as a prop', () => { @@ -118,7 +120,7 @@ describe('PackagesApp', () => { packageEntity: {}, }); - expect(emptyState()).toExist(); + expect(emptyState().exists()).toBe(true); }); it('package history has the right props', () => { @@ -152,7 +154,16 @@ describe('PackagesApp', () => { }); it('shows the delete confirmation modal when delete is clicked', () => { - expect(deleteModal()).toExist(); + expect(findDeleteModal().exists()).toBe(true); + }); + }); + + describe('deleting package files', () => { + it('shows the delete confirmation modal when delete is clicked', () => { + createComponent(); + findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); + + expect(findDeleteFileModal().exists()).toBe(true); }); }); @@ -228,13 +239,7 @@ describe('PackagesApp', () => { }); describe('tracking and delete', () => { - const doDelete = async () => { - deleteButton().trigger('click'); - await wrapper.vm.$nextTick(); - modalDeleteButton().trigger('click'); - }; - - describe('delete', () => { + describe('delete package', () => { const originalReferrer = document.referrer; const setReferrer = (value = defaultProjectName) => { Object.defineProperty(document, 'referrer', { @@ -250,9 +255,9 @@ describe('PackagesApp', () => { }); }); - it('calls the proper vuex action', async () => { + it('calls the proper vuex action', () => { createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); expect(deletePackage).toHaveBeenCalled(); }); @@ -260,7 +265,7 @@ describe('PackagesApp', () => { setReferrer(); deletePackage.mockResolvedValue(); createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); await deletePackage(); expect(window.location.replace).toHaveBeenCalledWith( 'project_url?showSuccessDeleteAlert=true', @@ -271,7 +276,7 @@ describe('PackagesApp', () => { setReferrer('baz'); deletePackage.mockResolvedValue(); createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); await deletePackage(); expect(window.location.replace).toHaveBeenCalledWith( 'group_url?showSuccessDeleteAlert=true', @@ -279,6 +284,17 @@ describe('PackagesApp', () => { }); }); + describe('delete file', () => { + it('calls the proper vuex action', () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); + findDeleteFileModal().vm.$emit('primary'); + + expect(deletePackageFile).toHaveBeenCalled(); + }); + }); + describe('tracking', () => { let eventSpy; let utilSpy; @@ -295,9 +311,9 @@ describe('PackagesApp', () => { expect(utilSpy).toHaveBeenCalledWith('conan'); }); - it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, async () => { + it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => { createComponent({ packageEntity: npmPackage }); - await doDelete(); + findDeleteModal().vm.$emit('primary'); expect(eventSpy).toHaveBeenCalledWith( category, TrackingActions.DELETE_PACKAGE, @@ -305,6 +321,56 @@ describe('PackagesApp', () => { ); }); + it(`canceling a package deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findDeleteModal().vm.$emit('canceled'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.CANCEL_DELETE_PACKAGE, + expect.any(Object), + ); + }); + + it(`request a file deletion tracks ${TrackingActions.REQUEST_DELETE_PACKAGE_FILE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', mavenFiles[0]); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.REQUEST_DELETE_PACKAGE_FILE, + expect.any(Object), + ); + }); + + it(`confirming a file deletion tracks ${TrackingActions.DELETE_PACKAGE_FILE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', npmPackage); + findDeleteFileModal().vm.$emit('primary'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.REQUEST_DELETE_PACKAGE_FILE, + expect.any(Object), + ); + }); + + it(`canceling a file deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE_FILE}`, () => { + createComponent({ packageEntity: npmPackage }); + + findPackageFiles().vm.$emit('delete-file', npmPackage); + findDeleteFileModal().vm.$emit('canceled'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + TrackingActions.CANCEL_DELETE_PACKAGE_FILE, + expect.any(Object), + ); + }); + it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => { createComponent({ packageEntity: conanPackage }); diff --git a/spec/frontend/packages/details/components/package_files_spec.js b/spec/frontend/packages/details/components/package_files_spec.js index bcf1b6d56f0..494aa631d9d 100644 --- a/spec/frontend/packages/details/components/package_files_spec.js +++ b/spec/frontend/packages/details/components/package_files_spec.js @@ -1,3 +1,4 @@ +import { GlDropdown } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import stubChildren from 'helpers/stub_children'; import component from '~/packages/details/components/package_files.vue'; @@ -12,16 +13,19 @@ describe('Package Files', () => { const findAllRows = () => wrapper.findAll('[data-testid="file-row"'); const findFirstRow = () => findAllRows().at(0); const findSecondRow = () => findAllRows().at(1); - const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"'); - const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"'); - const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"'); + const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]'); + const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]'); + const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]'); const findFirstRowFileIcon = () => findFirstRow().find(FileIcon); const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip); + const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown); + const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]'); - const createComponent = (packageFiles = npmFiles) => { + const createComponent = ({ packageFiles = npmFiles, canDelete = true } = {}) => { wrapper = mount(component, { propsData: { packageFiles, + canDelete, }, stubs: { ...stubChildren(component), @@ -43,7 +47,7 @@ describe('Package Files', () => { }); it('renders multiple files for a package that contains more than one file', () => { - createComponent(mavenFiles); + createComponent({ packageFiles: mavenFiles }); expect(findAllRows()).toHaveLength(2); }); @@ -123,7 +127,7 @@ describe('Package Files', () => { }); describe('when package file has no pipeline associated', () => { it('does not exist', () => { - createComponent(mavenFiles); + createComponent({ packageFiles: mavenFiles }); expect(findFirstRowCommitLink().exists()).toBe(false); }); @@ -131,11 +135,50 @@ describe('Package Files', () => { describe('when only one file lacks an associated pipeline', () => { it('renders the commit when it exists and not otherwise', () => { - createComponent([npmFiles[0], mavenFiles[0]]); + createComponent({ packageFiles: [npmFiles[0], mavenFiles[0]] }); expect(findFirstRowCommitLink().exists()).toBe(true); expect(findSecondRowCommitLink().exists()).toBe(false); }); }); + + describe('action menu', () => { + describe('when the user can delete', () => { + it('exists', () => { + createComponent(); + + expect(findFirstActionMenu().exists()).toBe(true); + }); + + describe('menu items', () => { + describe('delete file', () => { + it('exists', () => { + createComponent(); + + expect(findActionMenuDelete().exists()).toBe(true); + }); + + it('emits a delete event when clicked', () => { + createComponent(); + + findActionMenuDelete().vm.$emit('click'); + + const [[{ id }]] = wrapper.emitted('delete-file'); + expect(id).toBe(npmFiles[0].id); + }); + }); + }); + }); + + describe('when the user can not delete', () => { + const canDelete = false; + + it('does not exist', () => { + createComponent({ canDelete }); + + expect(findFirstActionMenu().exists()).toBe(false); + }); + }); + }); }); }); diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages/details/store/actions_spec.js index d11ee548b72..b16e50debc4 100644 --- a/spec/frontend/packages/details/store/actions_spec.js +++ b/spec/frontend/packages/details/store/actions_spec.js @@ -1,10 +1,18 @@ import testAction from 'helpers/vuex_action_helper'; import Api from '~/api'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants'; -import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions'; +import { + fetchPackageVersions, + deletePackage, + deletePackageFile, +} from '~/packages/details/store/actions'; import * as types from '~/packages/details/store/mutation_types'; -import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants'; +import { + DELETE_PACKAGE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, +} from '~/packages/shared/constants'; import { npmPackage as packageEntity } from '../../mock_data'; jest.mock('~/flash.js'); @@ -74,7 +82,10 @@ describe('Actions Package details store', () => { packageEntity.project_id, packageEntity.id, ); - expect(createFlash).toHaveBeenCalledWith(FETCH_PACKAGE_VERSIONS_ERROR); + expect(createFlash).toHaveBeenCalledWith({ + message: FETCH_PACKAGE_VERSIONS_ERROR, + type: 'warning', + }); done(); }, ); @@ -96,7 +107,48 @@ describe('Actions Package details store', () => { Api.deleteProjectPackage = jest.fn().mockRejectedValue(); testAction(deletePackage, undefined, { packageEntity }, [], [], () => { - expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + type: 'warning', + }); + done(); + }); + }); + }); + + describe('deletePackageFile', () => { + const fileId = 'a_file_id'; + + it('should call Api.deleteProjectPackageFile and commit the right data', (done) => { + const packageFiles = [{ id: 'foo' }, { id: fileId }]; + Api.deleteProjectPackageFile = jest.fn().mockResolvedValue(); + testAction( + deletePackageFile, + fileId, + { packageEntity, packageFiles }, + [{ type: types.UPDATE_PACKAGE_FILES, payload: [{ id: 'foo' }] }], + [], + () => { + expect(Api.deleteProjectPackageFile).toHaveBeenCalledWith( + packageEntity.project_id, + packageEntity.id, + fileId, + ); + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + type: 'success', + }); + done(); + }, + ); + }); + it('should create flash on API error', (done) => { + Api.deleteProjectPackageFile = jest.fn().mockRejectedValue(); + testAction(deletePackageFile, fileId, { packageEntity }, [], [], () => { + expect(createFlash).toHaveBeenCalledWith({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + type: 'warning', + }); done(); }); }); diff --git a/spec/frontend/packages/details/store/mutations_spec.js b/spec/frontend/packages/details/store/mutations_spec.js index 6bc5fb7241f..296ed02d786 100644 --- a/spec/frontend/packages/details/store/mutations_spec.js +++ b/spec/frontend/packages/details/store/mutations_spec.js @@ -28,4 +28,13 @@ describe('Mutations package details Store', () => { expect(mockState.packageEntity.versions).toEqual(fakeVersions); }); }); + describe('UPDATE_PACKAGE_FILES', () => { + it('should update the packageFiles', () => { + const files = [1, 2, 3]; + + mutations[types.UPDATE_PACKAGE_FILES](mockState, files); + + expect(mockState.packageFiles).toEqual(files); + }); + }); }); diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js index 79a0ab006da..03338b1930c 100644 --- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js +++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js @@ -1,4 +1,4 @@ -import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadio } from '@gitlab/ui'; +import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadio, GlFormSelect } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; @@ -89,6 +89,7 @@ describe('ForkForm component', () => { axiosMock.restore(); }); + const findFormSelect = () => wrapper.find(GlFormSelect); const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]'); const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]'); const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]'); @@ -229,6 +230,37 @@ describe('ForkForm component', () => { expect(wrapper.findAll(GlFormRadio)).toHaveLength(3); }); + it('resets the visibility to default "private" when the namespace is changed', async () => { + const namespaces = [ + { + visibility: 'private', + }, + { + visibility: 'internal', + }, + { + visibility: 'public', + }, + ]; + + mockGetRequest(); + createComponent( + { + projectVisibility: 'public', + }, + { + namespaces, + }, + ); + + expect(wrapper.vm.form.fields.visibility.value).toBe('public'); + findFormSelect().vm.$emit('input', namespaces[1]); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.form.fields.visibility.value).toBe('private'); + }); + it.each` project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} @@ -324,7 +356,6 @@ describe('ForkForm component', () => { await submitForm(); - expect(wrapper.find('[name="visibility"]:checked').exists()).toBe(false); expect(axios.post).not.toHaveBeenCalled(); }); }); diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index e345cd4de9b..a0a79dcad36 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -39,6 +39,45 @@ module Gitlab expect(expanded_config).to include(*included_config.keys) end end + + describe '#yaml_variables_for' do + let(:config_content) do + <<~YAML + variables: + VAR1: value 1 + VAR2: value 2 + + job: + script: echo 'hello' + variables: + VAR1: value 11 + YAML + end + + subject(:yaml_variables_for) { result.yaml_variables_for(:job) } + + it do + is_expected.to match_array([ + { key: 'VAR1', value: 'value 11', public: true }, + { key: 'VAR2', value: 'value 2', public: true } + ]) + end + end + + describe '#stage_for' do + let(:config_content) do + <<~YAML + job: + script: echo 'hello' + YAML + end + + subject(:stage_for) { result.stage_for(:job) } + + it do + is_expected.to eq('test') + end + end end end end diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb index 0fb3a69df05..8e02f4f562c 100644 --- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb +++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb @@ -25,34 +25,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi end context 'aggregated_metrics_data' do - shared_examples 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature' do - before do - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics) - end - end - - context 'with disabled database_sourced_aggregated_metrics feature flag' do - before do - stub_feature_flags(database_sourced_aggregated_metrics: false) - end - - let(:aggregated_metrics) do - [ - aggregated_metric(name: "gmau_2", source: "database", time_frame: time_frame) - ] - end - - it 'skips database sourced metrics', :aggregate_failures do - results = {} - params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at } - - expect(sources::PostgresHll).not_to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])) - expect(aggregated_metrics_data).to eq(results) - end - end - end - shared_examples 'aggregated_metrics_data' do context 'no aggregated metric is defined' do it 'returns empty hash' do @@ -237,7 +209,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi let(:time_frame) { ['all'] } it_behaves_like 'database_sourced_aggregated_metrics' - it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature' context 'redis sourced aggregated metrics' do let(:aggregated_metrics) { [aggregated_metric(name: 'gmau_1', time_frame: time_frame)] } @@ -278,7 +249,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi it_behaves_like 'database_sourced_aggregated_metrics' it_behaves_like 'redis_sourced_aggregated_metrics' - it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature' end describe '.aggregated_metrics_monthly_data' do @@ -289,7 +259,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi it_behaves_like 'database_sourced_aggregated_metrics' it_behaves_like 'redis_sourced_aggregated_metrics' - it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature' end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index c2f49b10639..d5694020b5f 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -320,6 +320,20 @@ RSpec.describe Ci::Build do end end + describe '#stick_build_if_status_changed' do + it 'sticks the build if the status changed' do + job = create(:ci_build, :pending) + + allow(Gitlab::Database::LoadBalancing).to receive(:enable?) + .and_return(true) + + expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:stick) + .with(:build, job.id) + + job.update!(status: :running) + end + end + describe '#enqueue' do let(:build) { create(:ci_build, :created) } diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 491088a44a1..ab7076e037e 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -273,6 +273,20 @@ RSpec.describe Ci::Runner do end end + describe '.recent' do + subject { described_class.recent } + + before do + @runner1 = create(:ci_runner, :instance, contacted_at: nil, created_at: 2.months.ago) + @runner2 = create(:ci_runner, :instance, contacted_at: nil, created_at: 3.months.ago) + @runner3 = create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 2.months.ago) + @runner4 = create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 3.months.ago) + @runner5 = create(:ci_runner, :instance, contacted_at: 3.months.ago, created_at: 5.months.ago) + end + + it { is_expected.to eq([@runner1, @runner3, @runner4])} + end + describe '.online' do subject { described_class.online } @@ -365,6 +379,22 @@ RSpec.describe Ci::Runner do it { is_expected.to eq([@runner1])} end + describe '#tick_runner_queue' do + it 'sticks the runner to the primary and calls the original method' do + runner = create(:ci_runner) + + allow(Gitlab::Database::LoadBalancing).to receive(:enable?) + .and_return(true) + + expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:stick) + .with(:runner, runner.id) + + expect(Gitlab::Workhorse).to receive(:set_key_and_notify) + + runner.tick_runner_queue + end + end + describe '#can_pick?' do using RSpec::Parameterized::TableSyntax diff --git a/spec/models/concerns/limitable_spec.rb b/spec/models/concerns/limitable_spec.rb index 753e2a8ee5e..6b25ed39efb 100644 --- a/spec/models/concerns/limitable_spec.rb +++ b/spec/models/concerns/limitable_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'active_model' RSpec.describe Limitable do let(:minimal_test_class) do @@ -35,6 +36,28 @@ RSpec.describe Limitable do instance.valid?(:create) end + + context 'with custom relation' do + before do + MinimalTestClass.limit_relation = :custom_relation + end + + it 'triggers custom limit_relation' do + instance = MinimalTestClass.new + + def instance.project + @project ||= Object.new + end + + limits = Object.new + custom_relation = Object.new + expect(instance).to receive(:custom_relation).and_return(custom_relation) + expect(instance.project).to receive(:actual_limits).and_return(limits) + expect(limits).to receive(:exceeded?).with(instance.class.name.demodulize.tableize, custom_relation).and_return(false) + + instance.valid?(:create) + end + end end context 'with global limit' do diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb index d8f1c98e762..3009f2bd56d 100644 --- a/spec/presenters/packages/detail/package_presenter_spec.rb +++ b/spec/presenters/packages/detail/package_presenter_spec.rb @@ -19,7 +19,8 @@ RSpec.describe ::Packages::Detail::PackagePresenter do size: file.size, file_md5: file.file_md5, file_sha1: file.file_sha1, - file_sha256: file.file_sha256 + file_sha256: file.file_sha256, + id: file.id } end end diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb index 3d5021fba08..8c95748aa5f 100644 --- a/spec/requests/api/ci/runner/jobs_put_spec.rb +++ b/spec/requests/api/ci/runner/jobs_put_spec.rb @@ -17,18 +17,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end describe '/api/v4/jobs' do - let(:group) { create(:group, :nested) } - let(:project) { create(:project, namespace: group, shared_runners_enabled: false) } - let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } - let(:runner) { create(:ci_runner, :project, projects: [project]) } - let(:user) { create(:user) } - let(:job) do - create(:ci_build, :artifacts, :extended_options, - pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) - end + let_it_be(:group) { create(:group, :nested) } + let_it_be(:project) { create(:project, namespace: group, shared_runners_enabled: false) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } + let_it_be(:user) { create(:user) } describe 'PUT /api/v4/jobs/:id' do - let(:job) do + let_it_be_with_reload(:job) do create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id) end @@ -204,53 +200,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end end - context 'when trace is given' do - it 'creates a trace artifact' do - allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do - ArchiveTraceWorker.new.perform(job.id) - end - - update_job(state: 'success', trace: 'BUILD TRACE UPDATED') - - job.reload - expect(response).to have_gitlab_http_status(:ok) - expect(job.trace.raw).to eq 'BUILD TRACE UPDATED' - expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED' - end - - context 'when concurrent update of trace is happening' do - before do - job.trace.write('wb') do - update_job(state: 'success', trace: 'BUILD TRACE UPDATED') - end - end - - it 'returns that operation conflicts' do - expect(response).to have_gitlab_http_status(:conflict) - end - end - end - - context 'when no trace is given' do - it 'does not override trace information' do - update_job - - expect(job.reload.trace.raw).to eq 'BUILD TRACE' - end - - context 'when running state is sent' do - it 'updates update_at value' do - expect { update_job_after_time }.to change { job.reload.updated_at } - end - end - - context 'when other state is sent' do - it "doesn't update update_at value" do - expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at } - end - end - end - context 'when job has been erased' do let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } @@ -267,20 +216,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do job.drop!(:script_failure) end - it 'does not update job status and job trace' do - update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + it 'does not update job status' do + update_job(state: 'success') job.reload expect(response).to have_gitlab_http_status(:forbidden) expect(response.header['Job-Status']).to eq 'failed' - expect(job.trace.raw).to eq 'Job failed' expect(job).to be_failed end end context 'when job does not exist anymore' do it 'returns 403 Forbidden' do - update_job(non_existing_record_id, state: 'success', trace: 'BUILD TRACE UPDATED') + update_job(non_existing_record_id, state: 'success') expect(response).to have_gitlab_http_status(:forbidden) end diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index b38630183f4..1696fe63d5d 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -94,7 +94,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when it exceeds the application limits' do before do - create(:ci_runner, runner_type: :project_type, projects: [project]) + create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago) create(:plan_limits, :default_plan, ci_registered_project_runners: 1) end @@ -106,6 +106,22 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(project.runners.reload.size).to eq(1) end end + + context 'when abandoned runners cause application limits to not be exceeded' do + before do + create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago) + create(:plan_limits, :default_plan, ci_registered_project_runners: 1) + end + + it 'creates runner' do + request + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['message']).to be_nil + expect(project.runners.reload.size).to eq(2) + expect(project.runners.recent.size).to eq(1) + end + end end context 'when group token is used' do @@ -135,7 +151,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when it exceeds the application limits' do before do - create(:ci_runner, runner_type: :group_type, groups: [group]) + create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago) create(:plan_limits, :default_plan, ci_registered_group_runners: 1) end @@ -147,6 +163,23 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(group.runners.reload.size).to eq(1) end end + + context 'when abandoned runners cause application limits to not be exceeded' do + before do + create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago) + create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago) + create(:plan_limits, :default_plan, ci_registered_group_runners: 1) + end + + it 'creates runner' do + request + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['message']).to be_nil + expect(group.runners.reload.size).to eq(3) + expect(group.runners.recent.size).to eq(1) + end + end end end diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb index 63190cc5d49..b0405226275 100644 --- a/spec/services/ci/update_build_state_service_spec.rb +++ b/spec/services/ci/update_build_state_service_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' RSpec.describe Ci::UpdateBuildStateService do - let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:metrics) { spy('metrics') } @@ -47,25 +48,6 @@ RSpec.describe Ci::UpdateBuildStateService do end end - context 'when request payload carries a trace' do - let(:params) { { state: 'success', trace: 'overwritten' } } - - it 'overwrites a trace' do - result = subject.execute - - expect(build.trace.raw).to eq 'overwritten' - expect(result.status).to eq 200 - end - - it 'updates overwrite operation metric' do - execute_with_stubbed_metrics! - - expect(metrics) - .to have_received(:increment_trace_operation) - .with(operation: :overwrite) - end - end - context 'when state is unknown' do let(:params) { { state: 'unknown' } } diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index 339efd31534..ccc64c80fd4 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -17,6 +17,28 @@ RSpec.shared_examples 'namespace traversal' do end end + describe '#root_ancestor' do + let_it_be(:group) { create(:group) } + let_it_be(:nested_group) { create(:group, parent: group) } + let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + + it 'returns the correct root ancestor' do + expect(group.root_ancestor).to eq(group) + expect(nested_group.root_ancestor).to eq(group) + expect(deep_nested_group.root_ancestor).to eq(group) + end + + describe '#recursive_root_ancestor' do + let(:groups) { [group, nested_group, deep_nested_group] } + + it "is equivalent to #recursive_root_ancestor" do + groups.each do |group| + expect(group.root_ancestor).to eq(group.recursive_root_ancestor) + end + end + end + end + describe '#self_and_hierarchy' do let!(:group) { create(:group, path: 'git_lab') } let!(:nested_group) { create(:group, parent: group) } |