diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-07 09:08:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-07 09:08:10 +0300 |
commit | f23de8014c9104ab62c68e88b4c8e924469cd996 (patch) | |
tree | a513f5c77fe54ff68309f1b0508422c5d2d3b39f /spec | |
parent | 896eadaa13c8852efa907d2555de883d07c38e48 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
9 files changed, 441 insertions, 11 deletions
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index 1697533803a..bb48d4dc38d 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -1,5 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; -import { GlForm } from '@gitlab/ui'; +import { GlForm, GlLoadingIcon } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -14,8 +14,14 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql'; +import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql'; +import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql'; import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers'; -import { createScheduleMutationResponse } from '../mock_data'; +import { + createScheduleMutationResponse, + updateScheduleMutationResponse, + mockSinglePipelineScheduleNode, +} from '../mock_data'; Vue.use(VueApollo); @@ -23,8 +29,20 @@ jest.mock('~/alert'); jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), joinPaths: jest.fn().mockReturnValue(''), + queryToObject: jest.fn().mockReturnValue({ id: '1' }), })); +const { + data: { + project: { + pipelineSchedules: { nodes }, + }, + }, +} = mockSinglePipelineScheduleNode; + +const schedule = nodes[0]; +const variables = schedule.variables.nodes; + describe('Pipeline schedules form', () => { let wrapper; const defaultBranch = 'main'; @@ -32,8 +50,13 @@ describe('Pipeline schedules form', () => { const cron = ''; const dailyLimit = ''; + const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode); + const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); + const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse); const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); + const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse); + const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); const createMockApolloProvider = ( requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]], @@ -52,8 +75,6 @@ describe('Pipeline schedules form', () => { fullPath: 'gitlab-org/gitlab', projectId, defaultBranch, - cron, - cronTimezone: '', dailyLimit, settingsLink: '', schedulesPath: '/root/ci-project/-/pipeline_schedules', @@ -69,6 +90,7 @@ describe('Pipeline schedules form', () => { const findRefSelector = () => wrapper.findComponent(RefSelector); const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button'); const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); // Variables const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row'); const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key'); @@ -187,7 +209,38 @@ describe('Pipeline schedules form', () => { }); }); - describe('schedule creation', () => { + describe('Button text', () => { + it.each` + editing | expectedText + ${true} | ${'Edit pipeline schedule'} + ${false} | ${'Create pipeline schedule'} + `( + 'button text is $expectedText when editing is $editing', + async ({ editing, expectedText }) => { + createComponent(shallowMountExtended, editing, [ + [getPipelineSchedulesQuery, querySuccessHandler], + ]); + + await waitForPromises(); + + expect(findSubmitButton().text()).toBe(expectedText); + }, + ); + }); + + describe('Schedule creation', () => { + it('when creating a schedule the query is not called', () => { + createComponent(); + + expect(querySuccessHandler).not.toHaveBeenCalled(); + }); + + it('does not show loading state when creating new schedule', () => { + createComponent(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + describe('schedule creation success', () => { let mock; @@ -259,4 +312,125 @@ describe('Pipeline schedules form', () => { }); }); }); + + describe('Schedule editing', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('shows loading state when editing', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + ]); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + describe('schedule fetch success', () => { + it('fetches schedule and sets form data correctly', async () => { + createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]); + + expect(querySuccessHandler).toHaveBeenCalled(); + + await waitForPromises(); + + expect(findDescription().element.value).toBe(schedule.description); + expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron); + expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone); + expect(findRefSelector().props('value')).toBe(schedule.ref); + expect(findVariableRows()).toHaveLength(3); + expect(findKeyInputs().at(0).element.value).toBe(variables[0].key); + expect(findKeyInputs().at(1).element.value).toBe(variables[1].key); + expect(findValueInputs().at(0).element.value).toBe(variables[0].value); + expect(findValueInputs().at(1).element.value).toBe(variables[1].value); + }); + }); + + it('schedule fetch failure', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, queryFailedHandler], + ]); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while trying to fetch the pipeline schedule.', + }); + }); + + it('edit schedule success', async () => { + createComponent(mountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + [updatePipelineScheduleMutation, updateMutationHandlerSuccess], + ]); + + await waitForPromises(); + + findDescription().element.value = 'Updated schedule'; + findDescription().trigger('change'); + + findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *'); + + // Ensures variable is sent with destroy property set true + findRemoveIcons().at(0).vm.$emit('click'); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({ + input: { + active: schedule.active, + cron: '0 22 16 * *', + cronTimezone: schedule.cronTimezone, + id: schedule.id, + ref: schedule.ref, + description: 'Updated schedule', + variables: [ + { + destroy: true, + id: variables[0].id, + key: variables[0].key, + value: variables[0].value, + variableType: variables[0].variableType, + }, + { + destroy: false, + id: variables[1].id, + key: variables[1].key, + value: variables[1].value, + variableType: variables[1].variableType, + }, + ], + }, + }); + }); + + it('edit schedule failure', async () => { + createComponent(shallowMountExtended, true, [ + [getPipelineSchedulesQuery, querySuccessHandler], + [updatePipelineScheduleMutation, updateMutationHandlerFailed], + ]); + + await waitForPromises(); + + findSubmitButton().vm.$emit('click'); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred while updating the pipeline schedule.', + }); + }); + }); }); diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js index 58fc4a616e4..81283a7170b 100644 --- a/spec/frontend/ci/pipeline_schedules/mock_data.js +++ b/spec/frontend/ci/pipeline_schedules/mock_data.js @@ -2,6 +2,7 @@ import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json'; import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json'; import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json'; +import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json'; const { data: { @@ -30,10 +31,10 @@ const { export const mockPipelineScheduleNodes = nodes; export const mockPipelineScheduleCurrentUser = currentUser; - export const mockPipelineScheduleAsGuestNodes = guestNodes; - export const mockTakeOwnershipNodes = takeOwnershipNodes; +export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse; + export const emptyPipelineSchedulesResponse = { data: { project: { @@ -89,4 +90,14 @@ export const createScheduleMutationResponse = { }, }; +export const updateScheduleMutationResponse = { + data: { + pipelineScheduleUpdate: { + clientMutationId: null, + errors: [], + __typename: 'PipelineScheduleUpdatePayload', + }, + }, +}; + export { mockGetPipelineSchedulesGraphQLResponse }; diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb index 3bfe9113e83..7bba7910b87 100644 --- a/spec/frontend/fixtures/pipeline_schedules.rb +++ b/spec/frontend/fixtures/pipeline_schedules.rb @@ -63,6 +63,12 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do expect_graphql_errors_to_be_empty end + it "#{fixtures_path}#{get_pipeline_schedules_query}.single.json" do + post_graphql(query, current_user: user, variables: { projectPath: project.full_path, ids: pipeline_schedule_populated.id }) + + expect_graphql_errors_to_be_empty + end + it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do guest = create(:user) project.add_guest(user) diff --git a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb index 564bc95b352..a932002d614 100644 --- a/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb +++ b/spec/graphql/mutations/ci/pipeline_schedule/variable_input_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' RSpec.describe Mutations::Ci::PipelineSchedule::VariableInputType, feature_category: :continuous_integration do specify { expect(described_class.graphql_name).to eq('PipelineScheduleVariableInput') } - it { expect(described_class.arguments.keys).to match_array(%w[key value variableType]) } + it { expect(described_class.arguments.keys).to match_array(%w[id key value variableType destroy]) } end diff --git a/spec/helpers/ci/pipeline_schedules_helper_spec.rb b/spec/helpers/ci/pipeline_schedules_helper_spec.rb new file mode 100644 index 00000000000..1ba24a08b58 --- /dev/null +++ b/spec/helpers/ci/pipeline_schedules_helper_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integration do + let_it_be(:project) { build_stubbed(:project) } + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:pipeline_schedule) { build_stubbed(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:timezones) { [{ identifier: "Pacific/Honolulu", name: "Hawaii" }] } + + let_it_be(:pipeline_schedule_variable) do + build_stubbed(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + + describe '#js_pipeline_schedules_form_data' do + before do + allow(helper).to receive(:timezone_data).and_return(timezones) + end + + it 'returns pipeline schedule form data' do + expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)).to include({ + full_path: project.full_path, + daily_limit: nil, + project_id: project.id, + schedules_path: pipeline_schedules_path(project), + settings_link: project_settings_ci_cd_path(project), + timezone_data: timezones.to_json + }) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb new file mode 100644 index 00000000000..8f7d5f25a80 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_missing_ci_cd_settings_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillMissingCiCdSettings, schema: 20230628023103, feature_category: :source_code_management do # rubocop:disable Layout/LineLength + let(:projects_table) { table(:projects) } + let(:namespaces_table) { table(:namespaces) } + let(:ci_cd_settings_table) { table(:project_ci_cd_settings) } + + let(:namespace_1) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-1') } + + let(:project_namespace_2) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-2', type: 'Project') } + let(:project_namespace_3) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-3', type: 'Project') } + let(:project_namespace_4) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') } + let(:project_namespace_5) { namespaces_table.create!(name: 'namespace', path: 'namespace-path-4', type: 'Project') } + let!(:project_1) do + projects_table + .create!( + name: 'project1', + path: 'path1', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_2.id, + visibility_level: 0 + ) + end + + let!(:project_2) do + projects_table + .create!( + name: 'project2', + path: 'path2', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_3.id, + visibility_level: 0 + ) + end + + let!(:project_3) do + projects_table + .create!( + name: 'project3', + path: 'path3', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_4.id, + visibility_level: 0 + ) + end + + let!(:ci_cd_settings_3) do + ci_cd_settings_table.create!(project_id: project_3.id) + end + + let!(:project_4) do + projects_table + .create!( + name: 'project4', + path: 'path4', + namespace_id: namespace_1.id, + project_namespace_id: project_namespace_5.id, + visibility_level: 0 + ) + end + + subject(:perform_migration) do + described_class.new(start_id: projects_table.minimum(:id), + end_id: projects_table.maximum(:id), + batch_table: :projects, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 0, + connection: projects_table.connection) + .perform + end + + it 'creates ci_cd_settings for projects without ci_cd_settings' do + expect { subject }.to change { ci_cd_settings_table.count }.from(1).to(4) + end + + it 'creates ci_cd_settings with default values' do + ci_cd_settings_table.where.not(project_id: ci_cd_settings_3.project_id).each do |ci_cd_setting| + expect(ci_cd_setting.attributes.except('id', 'project_id')).to eq({ + "group_runners_enabled" => true, + "merge_pipelines_enabled" => nil, + "default_git_depth" => 20, + "forward_deployment_enabled" => true, + "merge_trains_enabled" => false, + "auto_rollback_enabled" => false, + "keep_latest_artifact" => false, + "restrict_user_defined_variables" => false, + "job_token_scope_enabled" => false, + "runner_token_expiration_interval" => nil, + "separated_caches" => true, + "allow_fork_pipelines_to_run_in_parent_project" => true, + "inbound_job_token_scope_enabled" => true + }) + end + end +end diff --git a/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb b/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb new file mode 100644 index 00000000000..f6c470260ff --- /dev/null +++ b/spec/migrations/20230628023103_queue_backfill_missing_ci_cd_settings_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillMissingCiCdSettings, feature_category: :source_code_management do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :projects, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb index c1da231a4a6..3c3dcfc0a2d 100644 --- a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_update_spec.rb @@ -9,6 +9,14 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:variable_one) do + create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + + let_it_be(:variable_two) do + create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule) + end + let(:mutation) do variables = { id: pipeline_schedule.to_global_id.to_s, @@ -30,6 +38,7 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati nodes { key value + variableType } } } @@ -88,8 +97,37 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati expect(mutation_response['pipelineSchedule']['refForDisplay']).to eq(pipeline_schedule_parameters[:ref]) - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['key']).to eq('AAA') - expect(mutation_response['pipelineSchedule']['variables']['nodes'][0]['value']).to eq('AAA123') + expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['key']).to eq('AAA') + expect(mutation_response['pipelineSchedule']['variables']['nodes'][2]['value']).to eq('AAA123') + end + end + + context 'when updating and removing variables' do + let(:pipeline_schedule_parameters) do + { + variables: [ + { key: 'ABC', value: "ABC123", variableType: 'ENV_VAR', destroy: false }, + { id: variable_one.to_global_id.to_s, + key: 'foo', value: "foovalue", + variableType: 'ENV_VAR', + destroy: true }, + { id: variable_two.to_global_id.to_s, key: 'newbar', value: "newbarvalue", variableType: 'ENV_VAR' } + ] + } + end + + it 'processes variables correctly' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['pipelineSchedule']['variables']['nodes']) + .to match_array( + [ + { "key" => 'newbar', "value" => 'newbarvalue', "variableType" => 'ENV_VAR' }, + { "key" => 'ABC', "value" => "ABC123", "variableType" => 'ENV_VAR' } + ] + ) end end diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb index 6899f6c7d63..c31a652ed93 100644 --- a/spec/services/ci/pipeline_schedules/update_service_spec.rb +++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb @@ -8,9 +8,16 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + let_it_be(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, + key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) + end + before_all do project.add_maintainer(user) project.add_reporter(reporter) + + pipeline_schedule.reload end describe "execute" do @@ -35,7 +42,10 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo description: 'updated_desc', ref: 'patch-x', active: false, - cron: '*/1 * * * *' + cron: '*/1 * * * *', + variables_attributes: [ + { id: pipeline_schedule_variable.id, key: 'bar', secret_value: 'barvalue' } + ] } end @@ -47,6 +57,42 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo .and change { pipeline_schedule.ref }.from('master').to('patch-x') .and change { pipeline_schedule.active }.from(true).to(false) .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *') + .and change { pipeline_schedule.variables.last.key }.from('foo').to('bar') + .and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue') + end + + context 'when creating a variable' do + let(:params) do + { + variables_attributes: [ + { key: 'ABC', secret_value: 'ABC123' } + ] + } + end + + it 'creates the new variable' do + expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(1) + + expect(pipeline_schedule.variables.last.key).to eq('ABC') + expect(pipeline_schedule.variables.last.value).to eq('ABC123') + end + end + + context 'when deleting a variable' do + let(:params) do + { + variables_attributes: [ + { + id: pipeline_schedule_variable.id, + _destroy: true + } + ] + } + end + + it 'deletes the existing variable' do + expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(-1) + end end it 'returns ServiceResponse.success' do |