diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-23 09:09:17 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-23 09:09:17 +0000 |
commit | b3647b2a67930e8aa3c1b1dd9bda29c368c862ba (patch) | |
tree | b5903a88d38e6891bf4b5f7fdcedb5dcb34955b6 /spec | |
parent | fe6c2b9ae0af6aef067853fb20bef3d72e7978b8 (diff) | |
download | gitlab-ce-b3647b2a67930e8aa3c1b1dd9bda29c368c862ba.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
19 files changed, 470 insertions, 86 deletions
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 886be520668..b06d581d2c0 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -30,6 +30,21 @@ FactoryBot.define do yaml_variables { nil } end + trait :unique_name do + name { generate(:job_name) } + end + + trait :dependent do + transient do + sequence(:needed_name) { |n| "dependency #{n}" } + needed { association(:ci_build, name: needed_name, pipeline: pipeline) } + end + + after(:create) do |build, evaluator| + build.needs << create(:ci_build_need, build: build, name: evaluator.needed.name) + end + end + trait :started do started_at { 'Di 29. Okt 09:51:28 CET 2013' } end diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb index f9952cd9966..b276e6f8cfc 100644 --- a/spec/factories/sequences.rb +++ b/spec/factories/sequences.rb @@ -19,4 +19,5 @@ FactoryBot.define do sequence(:wip_title) { |n| "WIP: #{n}" } sequence(:jira_title) { |n| "[PROJ-#{n}]: fix bug" } sequence(:jira_branch) { |n| "feature/PROJ-#{n}" } + sequence(:job_name) { |n| "job #{n}" } end diff --git a/spec/frontend/__mocks__/vue/index.js b/spec/frontend/__mocks__/vue/index.js new file mode 100644 index 00000000000..52a5c6c5fcd --- /dev/null +++ b/spec/frontend/__mocks__/vue/index.js @@ -0,0 +1,7 @@ +import Vue from 'vue'; + +Vue.config.productionTip = false; +Vue.config.devtools = false; + +export default Vue; +export * from 'vue'; diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js index 2e253d24125..635964b6b4a 100644 --- a/spec/frontend/boards/components/issue_time_estimate_spec.js +++ b/spec/frontend/boards/components/issue_time_estimate_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { config as vueConfig } from 'vue'; +import Vue from 'vue'; import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; describe('Issue Time Estimate component', () => { @@ -34,10 +34,10 @@ describe('Issue Time Estimate component', () => { try { // This will raise props validating warning by Vue, silencing it - vueConfig.silent = true; + Vue.config.silent = true; await wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' }); } finally { - vueConfig.silent = false; + Vue.config.silent = false; } expect(alertSpy).not.toHaveBeenCalled(); diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js index a05e23a4250..00d557c11cf 100644 --- a/spec/frontend/feature_flags/components/form_spec.js +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -123,6 +123,10 @@ describe('feature flag form', () => { }); }); + it('has label', () => { + expect(findGlToggle().props('label')).toBe(Form.i18n.statusLabel); + }); + it('should be disabled if the feature flag is not active', (done) => { wrapper.setProps({ active: false }); wrapper.vm.$nextTick(() => { diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js index 2433c50ff24..859d3587223 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js @@ -59,7 +59,10 @@ describe('Maven Settings', () => { mountComponent(); expect(findToggle().exists()).toBe(true); - expect(findToggle().props('value')).toBe(defaultProps.mavenDuplicatesAllowed); + expect(findToggle().props()).toMatchObject({ + label: component.i18n.MAVEN_TOGGLE_LABEL, + value: defaultProps.mavenDuplicatesAllowed, + }); }); it('toggle emits an update event', () => { diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 0934dde8230..878721666ff 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -46,6 +46,7 @@ const defaultProps = { pagesHelpPath: '/help/user/project/pages/introduction#gitlab-pages-access-control', packagesAvailable: false, packagesHelpPath: '/help/user/packages/index', + requestCveAvailable: true, }; describe('Settings Panel', () => { @@ -76,6 +77,7 @@ describe('Settings Panel', () => { const findRepositoryFeatureSetting = () => findRepositoryFeatureProjectRow().find(projectFeatureSetting); const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' }); + const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' }); const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' }); const findProjectVisibilityLevelInput = () => wrapper.find('[name="project[visibility_level]"]'); const findRequestAccessEnabledInput = () => @@ -174,6 +176,16 @@ describe('Settings Panel', () => { }); }); + describe('Issues settings', () => { + it('has label for CVE request toggle', () => { + wrapper = mountComponent(); + + expect(findIssuesSettingsRow().findComponent(GlToggle).props('label')).toBe( + settingsPanel.i18n.cve_request_toggle_label, + ); + }); + }); + describe('Repository', () => { it('should set the repository help text when the visibility level is set to private', () => { wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }); @@ -304,6 +316,17 @@ describe('Settings Panel', () => { expect(findContainerRegistryEnabledInput().props('disabled')).toBe(true); }); + + it('has label for the toggle', () => { + wrapper = mountComponent({ + currentSettings: { visibilityLevel: visibilityOptions.PUBLIC }, + registryAvailable: true, + }); + + expect(findContainerRegistrySettings().findComponent(GlToggle).props('label')).toBe( + settingsPanel.i18n.containerRegistryLabel, + ); + }); }); describe('Git Large File Storage', () => { @@ -342,6 +365,15 @@ describe('Settings Panel', () => { expect(findLFSFeatureToggle().props('disabled')).toBe(true); }); + it('has label for toggle', () => { + wrapper = mountComponent({ + currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE }, + lfsAvailable: true, + }); + + expect(findLFSFeatureToggle().props('label')).toBe(settingsPanel.i18n.lfsLabel); + }); + it('should not change lfsEnabled when disabling the repository', async () => { // mount over shallowMount, because we are aiming to test rendered state of toggle wrapper = mountComponent({ currentSettings: { lfsEnabled: true } }, mount); @@ -432,6 +464,17 @@ describe('Settings Panel', () => { expect(findPackagesEnabledInput().props('disabled')).toBe(true); }); + + it('has label for toggle', () => { + wrapper = mountComponent({ + currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE }, + packagesAvailable: true, + }); + + expect(findPackagesEnabledInput().findComponent(GlToggle).props('label')).toBe( + settingsPanel.i18n.packagesLabel, + ); + }); }); describe('Pages', () => { diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js index 305dc557b39..40cfd785a20 100644 --- a/spec/frontend/pipelines/nav_controls_spec.js +++ b/spec/frontend/pipelines/nav_controls_spec.js @@ -1,17 +1,22 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import navControlsComp from '~/pipelines/components/pipelines_list/nav_controls.vue'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue'; describe('Pipelines Nav Controls', () => { - let NavControlsComponent; - let component; + let wrapper; - beforeEach(() => { - NavControlsComponent = Vue.extend(navControlsComp); - }); + const createComponent = (props) => { + wrapper = shallowMount(NavControls, { + propsData: { + ...props, + }, + }); + }; + + const findRunPipeline = () => wrapper.find('.js-run-pipeline'); afterEach(() => { - component.$destroy(); + wrapper.destroy(); }); it('should render link to create a new pipeline', () => { @@ -21,12 +26,11 @@ describe('Pipelines Nav Controls', () => { resetCachePath: 'foo', }; - component = mountComponent(NavControlsComponent, mockData); + createComponent(mockData); - expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline'); - expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual( - mockData.newPipelinePath, - ); + const runPipeline = findRunPipeline(); + expect(runPipeline.text()).toContain('Run Pipeline'); + expect(runPipeline.attributes('href')).toBe(mockData.newPipelinePath); }); it('should not render link to create pipeline if no path is provided', () => { @@ -36,9 +40,9 @@ describe('Pipelines Nav Controls', () => { resetCachePath: 'foo', }; - component = mountComponent(NavControlsComponent, mockData); + createComponent(mockData); - expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null); + expect(findRunPipeline().exists()).toBe(false); }); it('should render link for CI lint', () => { @@ -49,12 +53,10 @@ describe('Pipelines Nav Controls', () => { resetCachePath: 'foo', }; - component = mountComponent(NavControlsComponent, mockData); + createComponent(mockData); - expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint'); - expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual( - mockData.ciLintPath, - ); + expect(wrapper.find('.js-ci-lint').text().trim()).toContain('CI Lint'); + expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(mockData.ciLintPath); }); describe('Reset Runners Cache', () => { @@ -64,22 +66,20 @@ describe('Pipelines Nav Controls', () => { ciLintPath: 'foo', resetCachePath: 'foo', }; - - component = mountComponent(NavControlsComponent, mockData); + createComponent(mockData); }); it('should render button for resetting runner caches', () => { - expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain( - 'Clear Runner Caches', - ); + expect(wrapper.find('.js-clear-cache').text().trim()).toContain('Clear Runner Caches'); }); - it('should emit postAction event when reset runner cache button is clicked', () => { - jest.spyOn(component, '$emit').mockImplementation(() => {}); + it('should emit postAction event when reset runner cache button is clicked', async () => { + jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {}); - component.$el.querySelector('.js-clear-cache').click(); + wrapper.find('.js-clear-cache').vm.$emit('click'); + await nextTick(); - expect(component.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo'); + expect(wrapper.vm.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo'); }); }); }); diff --git a/spec/graphql/types/ci/job_status_enum_spec.rb b/spec/graphql/types/ci/job_status_enum_spec.rb new file mode 100644 index 00000000000..e8a1a2e0aa8 --- /dev/null +++ b/spec/graphql/types/ci/job_status_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiJobStatus'] do + it 'exposes all job status values' do + expect(described_class.values.values).to contain_exactly( + *::Ci::HasStatus::AVAILABLE_STATUSES.map do |status| + have_attributes(value: status, graphql_name: status.upcase) + end + ) + end +end diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index c54137a1c3e..4654b55eea5 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -8,16 +8,23 @@ RSpec.describe Types::Ci::JobType do it 'exposes the expected fields' do expected_fields = %i[ + allow_failure + artifacts + created_at + detailedStatus + duration + finished_at id - shortSha - pipeline name needs - detailedStatus + pipeline + queued_at scheduledAt - artifacts - finished_at - duration + scheduledAt + shortSha + stage + started_at + status ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index e0e84a1b635..13021c00aec 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Types::Ci::PipelineType do expected_fields = %w[ id iid sha before_sha status detailed_status config_source duration coverage created_at updated_at started_at finished_at committed_at - stages user retryable cancelable jobs source_job downstream + stages user retryable cancelable jobs job source_job downstream upstream path project active user_permissions warnings commit_path ] diff --git a/spec/graphql/types/ci/stage_type_spec.rb b/spec/graphql/types/ci/stage_type_spec.rb index 9a8d4fa96a3..cb8c1cb02cd 100644 --- a/spec/graphql/types/ci/stage_type_spec.rb +++ b/spec/graphql/types/ci/stage_type_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do name groups detailedStatus + jobs ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index 107f707ccd9..7e6ac351e68 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -100,4 +100,33 @@ RSpec.describe ApplicationRecord do expect(User.where_exists(User.limit(1))).to eq([user]) end end + + describe '.with_fast_read_statement_timeout' do + context 'when the query runs faster than configured timeout' do + it 'executes the query without error' do + result = nil + + expect do + described_class.with_fast_read_statement_timeout(100) do + result = described_class.connection.exec_query('SELECT 1') + end + end.not_to raise_error + + expect(result).not_to be_nil + end + end + + # This query hangs for 10ms and then gets cancelled. As there is no + # other way to test the timeout for sure, 10ms of waiting seems to be + # reasonable! + context 'when the query runs longer than configured timeout' do + it 'cancels the query and raises an exception' do + expect do + described_class.with_fast_read_statement_timeout(10) do + described_class.connection.exec_query('SELECT pg_sleep(0.1)') + end + end.to raise_error(ActiveRecord::QueryCanceled) + end + end + end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 0afc491dc73..677e4b34ecd 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -27,6 +27,18 @@ RSpec.describe Ci::Stage, :models do end end + describe '.by_name' do + it 'finds stages by name' do + a = create(:ci_stage_entity, name: 'a') + b = create(:ci_stage_entity, name: 'b') + c = create(:ci_stage_entity, name: 'c') + + expect(described_class.by_name('a')).to contain_exactly(a) + expect(described_class.by_name('b')).to contain_exactly(b) + expect(described_class.by_name(%w[a c])).to contain_exactly(a, c) + end + end + describe '#status' do context 'when stage is pending' do let(:stage) { create(:ci_stage_entity, status: 'pending') } diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb index 9e81358a152..4c063d359a5 100644 --- a/spec/requests/api/graphql/ci/groups_spec.rb +++ b/spec/requests/api/graphql/ci/groups_spec.rb @@ -4,10 +4,14 @@ require 'spec_helper' RSpec.describe 'Query.project.pipeline.stages.groups' do include GraphqlHelpers - let(:project) { create(:project, :repository, :public) } - let(:user) { create(:user) } - let(:pipeline) { create(:ci_pipeline, project: project, user: user) } - let(:group_graphql_data) { graphql_data.dig('project', 'pipeline', 'stages', 'nodes', 0, 'groups', 'nodes') } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let(:group_graphql_data) { graphql_data_at(:project, :pipeline, :stages, :nodes, 0, :groups, :nodes) } + + let_it_be(:job_a) { create(:commit_status, pipeline: pipeline, name: 'rspec 0 2') } + let_it_be(:job_b) { create(:ci_build, pipeline: pipeline, name: 'rspec 0 1') } + let_it_be(:job_c) { create(:ci_bridge, pipeline: pipeline, name: 'spinach 0 1') } let(:params) { {} } @@ -38,18 +42,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups' do end before do - create(:commit_status, pipeline: pipeline, name: 'rspec 0 2') - create(:commit_status, pipeline: pipeline, name: 'rspec 0 1') - create(:commit_status, pipeline: pipeline, name: 'spinach 0 1') post_graphql(query, current_user: user) end it_behaves_like 'a working graphql query' it 'returns a array of jobs belonging to a pipeline' do - expect(group_graphql_data.map { |g| g.slice('name', 'size') }).to eq([ - { 'name' => 'rspec', 'size' => 2 }, - { 'name' => 'spinach', 'size' => 1 } - ]) + expect(group_graphql_data).to contain_exactly( + a_hash_including('name' => 'rspec', 'size' => 2), + a_hash_including('name' => 'spinach', 'size' => 1) + ) end end diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb new file mode 100644 index 00000000000..78f7d3e149b --- /dev/null +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do + include GraphqlHelpers + + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') } + let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') } + + let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') } + let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') } + let_it_be(:job_3) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 3') } + + let(:path_to_job) do + [ + [:project, { full_path: project.full_path }], + [:pipelines, { first: 1 }], + [:nodes, nil], + [:job, { id: global_id_of(job_2) }] + ] + end + + let(:query) do + wrap_fields(query_graphql_path(query_path, all_graphql_fields_for(terminal_type))) + end + + describe 'scalar fields' do + let(:path) { [:project, :pipelines, :nodes, 0, :job] } + let(:query_path) { path_to_job } + let(:terminal_type) { 'CiJob' } + + it 'retrieves scalar fields' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(*path)).to match a_hash_including( + 'id' => global_id_of(job_2), + 'name' => job_2.name, + 'allowFailure' => job_2.allow_failure, + 'duration' => job_2.duration, + 'status' => job_2.status.upcase + ) + end + + context 'when fetching by name' do + before do + query_path.last[1] = { name: job_2.name } + end + + it 'retrieves scalar fields' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(*path)).to match a_hash_including( + 'id' => global_id_of(job_2), + 'name' => job_2.name + ) + end + end + end + + describe '.detailedStatus' do + let(:path) { [:project, :pipelines, :nodes, 0, :job, :detailed_status] } + let(:query_path) { path_to_job + [:detailed_status] } + let(:terminal_type) { 'DetailedStatus' } + + it 'retrieves detailed status' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(*path)).to match a_hash_including( + 'text' => 'pending', + 'label' => 'pending', + 'action' => a_hash_including('buttonTitle' => 'Cancel this job', 'icon' => 'cancel') + ) + end + end + + describe '.stage' do + let(:path) { [:project, :pipelines, :nodes, 0, :job, :stage] } + let(:query_path) { path_to_job + [:stage] } + let(:terminal_type) { 'CiStage' } + + it 'returns appropriate data' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(*path)).to match a_hash_including( + 'name' => test_stage.name, + 'jobs' => a_hash_including( + 'nodes' => contain_exactly( + a_hash_including('id' => global_id_of(job_2)), + a_hash_including('id' => global_id_of(job_3)) + ) + ) + ) + end + end +end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index cc028ff2ff9..6436fe1e9ef 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -5,24 +5,28 @@ require 'spec_helper' RSpec.describe 'getting pipeline information nested in a project' do include GraphqlHelpers - let!(:project) { create(:project, :repository, :public) } - let!(:pipeline) { create(:ci_pipeline, project: project) } - let!(:current_user) { create(:user) } - let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] } - - let!(:query) do - %( - query { - project(fullPath: "#{project.full_path}") { - pipeline(iid: "#{pipeline.iid}") { - configSource - } - } - } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:current_user) { create(:user) } + let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline) } + let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline) } + let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline) } + + let(:path) { %i[project pipeline] } + let(:pipeline_graphql_data) { graphql_data_at(*path) } + let(:depth) { 3 } + let(:excluded) { %w[job project] } # Project is very expensive, due to the number of fields + let(:fields) { all_graphql_fields_for('Pipeline', excluded: excluded, max_depth: depth) } + + let(:query) do + graphql_query_for( + :project, + { full_path: project.full_path }, + query_graphql_field(:pipeline, { iid: pipeline.iid.to_s }, fields) ) end - it_behaves_like 'a working graphql query' do + it_behaves_like 'a working graphql query', :use_clean_rails_memory_store_caching, :request_store do before do post_graphql(query, current_user: current_user) end @@ -37,14 +41,18 @@ RSpec.describe 'getting pipeline information nested in a project' do it 'contains configSource' do post_graphql(query, current_user: current_user) - expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE') + expect(pipeline_graphql_data['configSource']).to eq('UNKNOWN_SOURCE') end - context 'batching' do - let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) } - let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) } + context 'when batching' do + let!(:pipeline2) { successful_pipeline } + let!(:pipeline3) { successful_pipeline } let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) } + def successful_pipeline + create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) + end + it 'executes the finder once' do mock = double(Ci::PipelinesFinder) opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) } @@ -80,4 +88,151 @@ RSpec.describe 'getting pipeline information nested in a project' do graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields) end + + context 'when enough data is requested' do + let(:fields) do + query_graphql_field(:jobs, nil, + query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3))) + end + + it 'contains jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly( + a_hash_including( + 'name' => build_job.name, + 'status' => build_job.status.upcase, + 'duration' => build_job.duration + ), + a_hash_including( + 'id' => global_id_of(failed_build), + 'status' => failed_build.status.upcase + ), + a_hash_including( + 'id' => global_id_of(bridge), + 'status' => bridge.status.upcase + ) + ) + end + end + + context 'when requesting only builds with certain statuses' do + let(:variables) do + { + path: project.full_path, + pipelineIID: pipeline.iid.to_s, + status: :FAILED + } + end + + let(:query) do + <<~GQL + query($path: ID!, $pipelineIID: ID!, $status: CiJobStatus!) { + project(fullPath: $path) { + pipeline(iid: $pipelineIID) { + jobs(statuses: [$status]) { + nodes { + #{all_graphql_fields_for('CiJob', max_depth: 1)} + } + } + } + } + } + GQL + end + + it 'can filter build jobs by status' do + post_graphql(query, current_user: current_user, variables: variables) + + expect(graphql_data_at(*path, :jobs, :nodes)) + .to contain_exactly(a_hash_including('id' => global_id_of(failed_build))) + end + end + + context 'when requesting a specific job' do + let(:variables) do + { + path: project.full_path, + pipelineIID: pipeline.iid.to_s + } + end + + let(:build_fields) do + all_graphql_fields_for('CiJob', max_depth: 1) + end + + let(:query) do + <<~GQL + query($path: ID!, $pipelineIID: ID!, $jobName: String, $jobID: JobID) { + project(fullPath: $path) { + pipeline(iid: $pipelineIID) { + job(id: $jobID, name: $jobName) { + #{build_fields} + } + } + } + } + GQL + end + + let(:the_job) do + a_hash_including('name' => build_job.name, 'id' => global_id_of(build_job)) + end + + it 'can request a build by name' do + vars = variables.merge(jobName: build_job.name) + + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_data_at(*path, :job)).to match(the_job) + end + + it 'can request a build by ID' do + vars = variables.merge(jobID: global_id_of(build_job)) + + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_data_at(*path, :job)).to match(the_job) + end + + context 'when we request nested fields of the build' do + let_it_be(:needy) { create(:ci_build, :dependent, pipeline: pipeline) } + + let(:build_fields) { 'needs { nodes { name } }' } + let(:vars) { variables.merge(jobID: global_id_of(needy)) } + + it 'returns the nested data' do + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_data_at(*path, :job, :needs, :nodes)).to contain_exactly( + a_hash_including('name' => needy.needs.first.name) + ) + end + + it 'requires a constant number of queries' do + fst_user = create(:user) + snd_user = create(:user) + path = %i[project pipeline job needs nodes name] + + baseline = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: fst_user, variables: vars) + end + + expect(baseline.count).to be > 0 + dep_names = graphql_dig_at(graphql_data(fresh_response_data), *path) + + deps = create_list(:ci_build, 3, :unique_name, pipeline: pipeline) + deps.each { |d| create(:ci_build_need, build: needy, name: d.name) } + + expect do + post_graphql(query, current_user: snd_user, variables: vars) + end.not_to exceed_query_limit(baseline) + + more_names = graphql_dig_at(graphql_data(fresh_response_data), *path) + + expect(more_names).to include(*dep_names) + expect(more_names.count).to be > dep_names.count + end + end + end end diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb index 683ee3e4bf2..6e145fed733 100644 --- a/spec/support/helpers/board_helpers.rb +++ b/spec/support/helpers/board_helpers.rb @@ -5,14 +5,5 @@ module BoardHelpers within card do first('.board-card-number').click end - - wait_for_sidebar - end - - def wait_for_sidebar - # loop until the CSS transition is complete - Timeout.timeout(0.5) do - loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290 - end end end diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index 565c21e0f85..904b7efdd7f 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -30,11 +30,13 @@ RSpec::Matchers.define :have_graphql_fields do |*expected| end match do |kls| - if @allow_extra - expect(kls.fields.keys).to include(*expected_field_names) - else - expect(kls.fields.keys).to contain_exactly(*expected_field_names) - end + keys = kls.fields.keys.to_set + fields = expected_field_names.to_set + + next true if fields == keys + next true if @allow_extra && fields.proper_subset?(keys) + + false end failure_message do |kls| @@ -108,7 +110,7 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected| names = expected_names(field).inspect args = field.arguments.keys.inspect - "expected that #{field.name} would have the following arguments: #{names}, but it has #{args}." + "expected #{field.name} to have the following arguments: #{names}, but it has #{args}." end end |