diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-13 15:09:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-13 15:09:54 +0300 |
commit | affec3ced2d85697d9a21d83e811285b030705f2 (patch) | |
tree | db315bff6a2e4a2f3db1509eb573c082b046a320 /spec | |
parent | 152d3b652dd7daad10b3056f47e579acf601546d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
20 files changed, 463 insertions, 36 deletions
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index b6efced698d..93efce6b58b 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -470,7 +470,7 @@ RSpec.describe DiffHelper do end describe '#conflicts' do - let(:merge_request) { instance_double(MergeRequest) } + let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: true) } let(:merge_ref_head_diff) { true } let(:can_be_resolved_in_ui?) { true } let(:allow_tree_conflicts) { false } @@ -504,6 +504,14 @@ RSpec.describe DiffHelper do end end + context 'when merge request can be merged' do + let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: false) } + + it 'returns nil' do + expect(helper.conflicts).to be_nil + end + end + context 'when conflicts cannot be resolved in UI' do let(:can_be_resolved_in_ui?) { false } diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb index f8b3a8681f0..98866bb765f 100644 --- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb @@ -92,5 +92,69 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do end end end + + context 'when the subclass uses distinct each batch' do + let(:job_instance) do + job_class.new(start_id: 1, + end_id: 100, + batch_table: '_test_table', + batch_column: 'from_column', + sub_batch_size: 2, + pause_ms: 10, + connection: connection) + end + + let(:job_class) do + Class.new(described_class) do + def perform(*job_arguments) + distinct_each_batch(operation_name: :insert) do |sub_batch| + sub_batch.pluck(:from_column).each do |value| + connection.execute("INSERT INTO _test_insert_table VALUES (#{value})") + end + + sub_batch.size + end + end + end + end + + let(:test_table) { table(:_test_table) } + let(:test_insert_table) { table(:_test_insert_table) } + + before do + allow(job_instance).to receive(:sleep) + + connection.create_table :_test_table do |t| + t.timestamps_with_timezone null: false + t.integer :from_column, null: false + end + + connection.create_table :_test_insert_table, id: false do |t| + t.integer :to_column + t.index :to_column, unique: true + end + + test_table.create!(id: 1, from_column: 5) + test_table.create!(id: 2, from_column: 10) + test_table.create!(id: 3, from_column: 10) + test_table.create!(id: 4, from_column: 5) + test_table.create!(id: 5, from_column: 15) + end + + after do + connection.drop_table(:_test_table) + connection.drop_table(:_test_insert_table) + end + + it 'calls the operation for each distinct batch' do + expect { perform_job }.to change { test_insert_table.pluck(:to_column) }.from([]).to([5, 10, 15]) + end + + it 'stores the affected rows' do + perform_job + + expect(job_instance.batch_metrics.affected_rows[:insert]).to contain_exactly(2, 1) + end + end end end diff --git a/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb b/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb new file mode 100644 index 00000000000..1a00fd7c8b3 --- /dev/null +++ b/spec/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::LooseIndexScanBatchingStrategy, '#next_batch' do + let(:batching_strategy) { described_class.new(connection: ActiveRecord::Base.connection) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + + let!(:namespace1) { namespaces.create!(name: 'ns1', path: 'ns1') } + let!(:namespace2) { namespaces.create!(name: 'ns2', path: 'ns2') } + let!(:namespace3) { namespaces.create!(name: 'ns3', path: 'ns3') } + let!(:namespace4) { namespaces.create!(name: 'ns4', path: 'ns4') } + let!(:namespace5) { namespaces.create!(name: 'ns5', path: 'ns5') } + let!(:project1) { projects.create!(name: 'p1', namespace_id: namespace1.id, project_namespace_id: namespace1.id) } + let!(:project2) { projects.create!(name: 'p2', namespace_id: namespace2.id, project_namespace_id: namespace2.id) } + let!(:project3) { projects.create!(name: 'p3', namespace_id: namespace3.id, project_namespace_id: namespace3.id) } + let!(:project4) { projects.create!(name: 'p4', namespace_id: namespace4.id, project_namespace_id: namespace4.id) } + let!(:project5) { projects.create!(name: 'p5', namespace_id: namespace5.id, project_namespace_id: namespace5.id) } + + let!(:issue1) { issues.create!(title: 'title', description: 'description', project_id: project2.id) } + let!(:issue2) { issues.create!(title: 'title', description: 'description', project_id: project1.id) } + let!(:issue3) { issues.create!(title: 'title', description: 'description', project_id: project2.id) } + let!(:issue4) { issues.create!(title: 'title', description: 'description', project_id: project3.id) } + let!(:issue5) { issues.create!(title: 'title', description: 'description', project_id: project2.id) } + let!(:issue6) { issues.create!(title: 'title', description: 'description', project_id: project4.id) } + let!(:issue7) { issues.create!(title: 'title', description: 'description', project_id: project5.id) } + + it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchingStrategies::BaseStrategy } + + context 'when starting on the first batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project1.id, batch_size: 2, job_arguments: []) + + expect(batch_bounds).to eq([project1.id, project2.id]) + end + end + + context 'when additional batches remain' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project2.id, batch_size: 3, job_arguments: []) + + expect(batch_bounds).to eq([project2.id, project4.id]) + end + end + + context 'when on the final batch' do + it 'returns the bounds of the next batch' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project4.id, batch_size: 3, job_arguments: []) + + expect(batch_bounds).to eq([project4.id, project5.id]) + end + end + + context 'when no additional batches remain' do + it 'returns nil' do + batch_bounds = batching_strategy + .next_batch(:issues, :project_id, batch_min_value: project5.id + 1, batch_size: 1, job_arguments: []) + + expect(batch_bounds).to be_nil + end + end +end diff --git a/spec/models/operations/feature_flags_client_spec.rb b/spec/models/operations/feature_flags_client_spec.rb index 05988d676f3..2ed3222c65c 100644 --- a/spec/models/operations/feature_flags_client_spec.rb +++ b/spec/models/operations/feature_flags_client_spec.rb @@ -3,7 +3,15 @@ require 'spec_helper' RSpec.describe Operations::FeatureFlagsClient do - subject { create(:operations_feature_flags_client) } + let_it_be(:project) { create(:project) } + + let!(:client) { create(:operations_feature_flags_client, project: project) } + + subject { client } + + before do + client.unleash_app_name = 'production' + end describe 'associations' do it { is_expected.to belong_to(:project) } @@ -18,4 +26,64 @@ RSpec.describe Operations::FeatureFlagsClient do expect(subject.token).not_to be_empty end end + + describe '.update_last_feature_flag_updated_at!' do + subject { described_class.update_last_feature_flag_updated_at!(project) } + + it 'updates the last_feature_flag_updated_at of the project client' do + freeze_time do + expect { subject }.to change { client.reload.last_feature_flag_updated_at }.from(nil).to(Time.current) + end + end + end + + describe '#unleash_api_version' do + subject { client.unleash_api_version } + + it { is_expected.to eq(described_class::DEFAULT_UNLEASH_API_VERSION) } + end + + describe '#unleash_api_features' do + subject { client.unleash_api_features } + + it 'fetches' do + expect(Operations::FeatureFlag).to receive(:for_unleash_client).with(project, 'production').once + + subject + end + + context 'when unleash app name is not set' do + before do + client.unleash_app_name = nil + end + + it 'does not fetch' do + expect(Operations::FeatureFlag).not_to receive(:for_unleash_client) + + subject + end + end + end + + describe '#unleash_api_cache_key' do + subject { client.unleash_api_cache_key } + + it 'constructs the cache key' do + is_expected.to eq("api_version:#{client.unleash_api_version}"\ + ":app_name:#{client.unleash_app_name}"\ + ":updated_at:#{client.last_feature_flag_updated_at.to_i}") + end + + context 'when unleash app name is not set' do + before do + client.unleash_app_name = nil + end + + it 'constructs the cache key without unleash app name' do + is_expected.to eq("api_version:#{client.unleash_api_version}"\ + ":app_name:"\ + ":updated_at:#{client.last_feature_flag_updated_at.to_i}") + end + end + end end diff --git a/spec/requests/api/feature_flags_user_lists_spec.rb b/spec/requests/api/feature_flags_user_lists_spec.rb index e2a3f92df10..bfc57042ff4 100644 --- a/spec/requests/api/feature_flags_user_lists_spec.rb +++ b/spec/requests/api/feature_flags_user_lists_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::FeatureFlagsUserLists do let_it_be(:project, refind: true) { create(:project) } + let_it_be(:client, refind: true) { create(:operations_feature_flags_client, project: project) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } @@ -215,6 +216,7 @@ RSpec.describe API::FeatureFlagsUserLists do } expect(response).to have_gitlab_http_status(:forbidden) + expect(client.reload.last_feature_flag_updated_at).to be_nil end it 'creates the flag' do @@ -231,6 +233,7 @@ RSpec.describe API::FeatureFlagsUserLists do }) expect(project.operations_feature_flags_user_lists.count).to eq(1) expect(project.operations_feature_flags_user_lists.last.name).to eq('mylist') + expect(client.reload.last_feature_flag_updated_at).not_to be_nil end it 'requires name' do @@ -298,6 +301,7 @@ RSpec.describe API::FeatureFlagsUserLists do } expect(response).to have_gitlab_http_status(:forbidden) + expect(client.reload.last_feature_flag_updated_at).to be_nil end it 'updates the list' do @@ -313,6 +317,7 @@ RSpec.describe API::FeatureFlagsUserLists do 'user_xids' => '456,789' }) expect(list.reload.name).to eq('mylist') + expect(client.reload.last_feature_flag_updated_at).not_to be_nil end it 'preserves attributes not listed in the request' do @@ -377,6 +382,7 @@ RSpec.describe API::FeatureFlagsUserLists do expect(response).to have_gitlab_http_status(:not_found) expect(json_response).to eq({ 'message' => '404 Not found' }) + expect(client.reload.last_feature_flag_updated_at).to be_nil end it 'deletes the list' do @@ -387,6 +393,7 @@ RSpec.describe API::FeatureFlagsUserLists do expect(response).to have_gitlab_http_status(:no_content) expect(response.body).to be_blank expect(project.operations_feature_flags_user_lists.count).to eq(0) + expect(client.reload.last_feature_flag_updated_at).not_to be_nil end it 'does not delete the list if it is associated with a strategy' do diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index ee818d9e37c..c33dda67a25 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -124,6 +124,16 @@ RSpec.describe 'Create a work item' do expect(mutation_response['workItem']).to be_nil end end + + context 'when parent work item is not found' do + let_it_be(:parent) { build_stubbed(:work_item, id: non_existing_record_id)} + + it 'returns a top level error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.first['message']).to include('No object found for `parentId') + end + end end end diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 58549c6e3a0..e2c4386772c 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -166,13 +166,13 @@ RSpec.describe 'Update a work item' do context 'when updating parent' do let_it_be(:work_item) { create(:work_item, :task, project: project) } + let_it_be(:valid_parent) { create(:work_item, project: project) } + let_it_be(:invalid_parent) { create(:work_item, :task, project: project) } context 'when parent work item type is invalid' do - let_it_be(:parent_task) { create(:work_item, :task, project: project) } - let(:error) { "#{work_item.to_reference} cannot be added: Only Issue can be parent of Task." } let(:input) do - { 'hierarchyWidget' => { 'parentId' => parent_task.to_global_id.to_s }, 'title' => 'new title' } + { 'hierarchyWidget' => { 'parentId' => invalid_parent.to_global_id.to_s }, 'title' => 'new title' } end it 'returns response with errors' do @@ -187,21 +187,19 @@ RSpec.describe 'Update a work item' do end context 'when parent work item has a valid type' do - let_it_be(:parent) { create(:work_item, project: project) } - - let(:input) { { 'hierarchyWidget' => { 'parentId' => parent.to_global_id.to_s } } } + let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } } it 'sets the parent for the work item' do expect do post_graphql_mutation(mutation, current_user: current_user) work_item.reload - end.to change(work_item, :work_item_parent).from(nil).to(parent) + end.to change(work_item, :work_item_parent).from(nil).to(valid_parent) expect(response).to have_gitlab_http_status(:success) expect(widgets_response).to include( { 'children' => { 'edges' => [] }, - 'parent' => { 'id' => parent.to_global_id.to_s }, + 'parent' => { 'id' => valid_parent.to_global_id.to_s }, 'type' => 'HIERARCHY' } ) @@ -218,10 +216,62 @@ RSpec.describe 'Update a work item' do expect do post_graphql_mutation(mutation, current_user: current_user) work_item.reload - end.to change(work_item, :work_item_parent).from(existing_parent).to(parent) + end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent) + end + end + end + + context 'when parentId is null' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => nil } } } + + context 'when parent is present' do + before do + work_item.update!(work_item_parent: valid_parent) + end + + it 'removes parent and returns success message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(valid_parent).to(nil) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response) + .to include( + { + 'children' => { 'edges' => [] }, + 'parent' => nil, + 'type' => 'HIERARCHY' + } + ) + end + end + + context 'when parent is not present' do + before do + work_item.update!(work_item_parent: nil) + end + + it 'does not change work item and returns success message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.not_to change(work_item, :work_item_parent) + + expect(response).to have_gitlab_http_status(:success) end end end + + context 'when parent work item is not found' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => "gid://gitlab/WorkItem/#{non_existing_record_id}" } } } + + it 'returns a top level error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.first['message']).to include('No object found for `parentId') + end + end end context 'when updating children' do diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb index 6cb801538c6..7bdb89fb286 100644 --- a/spec/requests/api/unleash_spec.rb +++ b/spec/requests/api/unleash_spec.rb @@ -168,7 +168,7 @@ RSpec.describe API::Unleash do end %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint| - describe "GET #{features_endpoint}" do + describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) } let(:client) { create(:operations_feature_flags_client, project: project) } @@ -176,6 +176,46 @@ RSpec.describe API::Unleash do it_behaves_like 'authenticated request' + context 'when a client fetches feature flags several times' do + let(:headers) { { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } } + + before do + create_list(:operations_feature_flag, 3, project: project) + end + + it 'serializes feature flags for the first time and read cached data from the second time' do + expect(API::Entities::Unleash::ClientFeatureFlags) + .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args) + .once + + 5.times { get api(features_url), params: params, headers: headers } + end + + it 'increments the cache key when feature flags are modified' do + expect(API::Entities::Unleash::ClientFeatureFlags) + .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args) + .twice + + 2.times { get api(features_url), params: params, headers: headers } + + ::FeatureFlags::CreateService.new(project, project.owner, name: 'feature_flag').execute + + 3.times { get api(features_url), params: params, headers: headers } + end + + context 'when cache_unleash_client_api is disabled' do + before do + stub_feature_flags(cache_unleash_client_api: false) + end + + it 'serializes feature flags every time' do + expect(::API::Entities::UnleashFeature).to receive(:represent).exactly(5).times + + 5.times { get api(features_url), params: params, headers: headers } + end + end + end + context 'with version 2 feature flags' do it 'does not return a flag without any strategies' do create(:operations_feature_flag, project: project, diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb index aef7d3732f8..72777bde30c 100644 --- a/spec/serializers/diffs_entity_spec.rb +++ b/spec/serializers/diffs_entity_spec.rb @@ -100,6 +100,7 @@ RSpec.describe DiffsEntity do let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) } before do + allow(merge_request).to receive(:cannot_be_merged?).and_return(true) allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts) end diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb index 3311b434ce5..0e3d808aaac 100644 --- a/spec/serializers/diffs_metadata_entity_spec.rb +++ b/spec/serializers/diffs_metadata_entity_spec.rb @@ -65,6 +65,7 @@ RSpec.describe DiffsMetadataEntity do let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: false) } before do + allow(merge_request).to receive(:cannot_be_merged?).and_return(true) allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts) end diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb index db8bf92cbf5..9d4456c11d6 100644 --- a/spec/serializers/paginated_diff_entity_spec.rb +++ b/spec/serializers/paginated_diff_entity_spec.rb @@ -43,6 +43,7 @@ RSpec.describe PaginatedDiffEntity do let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) } before do + allow(merge_request).to receive(:cannot_be_merged?).and_return(true) allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts) end diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb index 8a225494d5a..fd9a89fe8e2 100644 --- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb +++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb @@ -32,6 +32,12 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do + expect(Ci::RunnerVersion).to receive(:insert_all) + .ordered + .with([{ version: '14.0.2' }], anything) + .once + .and_call_original + result = nil expect { result = execute } .to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended') diff --git a/spec/services/feature_flags/create_service_spec.rb b/spec/services/feature_flags/create_service_spec.rb index e37d41562f9..1c9bde70af3 100644 --- a/spec/services/feature_flags/create_service_spec.rb +++ b/spec/services/feature_flags/create_service_spec.rb @@ -41,6 +41,8 @@ RSpec.describe FeatureFlags::CreateService do subject end + + it_behaves_like 'does not update feature flag client' end context 'when feature flag is saved correctly' do @@ -62,6 +64,8 @@ RSpec.describe FeatureFlags::CreateService do expect { subject }.to change { Operations::FeatureFlag.count }.by(1) end + it_behaves_like 'update feature flag client' + context 'when Jira Connect subscription does not exist' do it 'does not sync the feature flag to Jira' do expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async) diff --git a/spec/services/feature_flags/destroy_service_spec.rb b/spec/services/feature_flags/destroy_service_spec.rb index d3796ef6b4d..740923db9b6 100644 --- a/spec/services/feature_flags/destroy_service_spec.rb +++ b/spec/services/feature_flags/destroy_service_spec.rb @@ -36,6 +36,8 @@ RSpec.describe FeatureFlags::DestroyService do expect(audit_event_message).to eq("Deleted feature flag #{feature_flag.name}.") end + it_behaves_like 'update feature flag client' + context 'when user is reporter' do let(:user) { reporter } @@ -57,6 +59,8 @@ RSpec.describe FeatureFlags::DestroyService do it 'does not create audit log' do expect { subject }.not_to change { AuditEvent.count } end + + it_behaves_like 'does not update feature flag client' end end end diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb index f5e94c4af0f..8f985d34961 100644 --- a/spec/services/feature_flags/update_service_spec.rb +++ b/spec/services/feature_flags/update_service_spec.rb @@ -58,6 +58,8 @@ RSpec.describe FeatureFlags::UpdateService do ) end + it_behaves_like 'update feature flag client' + context 'with invalid params' do let(:params) { { name: nil } } @@ -79,6 +81,8 @@ RSpec.describe FeatureFlags::UpdateService do subject end + + it_behaves_like 'does not update feature flag client' end context 'when user is reporter' do diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 2cf5c60abd9..0d3c5391a4f 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe WorkItems::CreateService do include AfterNextHelpers let_it_be_with_reload(:project) { create(:project) } + let_it_be(:parent) { create(:work_item, project: project) } let_it_be(:guest) { create(:user) } let_it_be(:user_with_no_access) { create(:user) } @@ -93,7 +94,7 @@ RSpec.describe WorkItems::CreateService do it_behaves_like 'work item widgetable service' do let(:widget_params) do { - hierarchy_widget: { parent_id: 1 } + hierarchy_widget: { parent: parent } } end @@ -111,16 +112,18 @@ RSpec.describe WorkItems::CreateService do let(:supported_widgets) do [ - { klass: WorkItems::Widgets::HierarchyService::CreateService, callback: :after_create_in_transaction, params: { parent_id: 1 } } + { + klass: WorkItems::Widgets::HierarchyService::CreateService, + callback: :after_create_in_transaction, + params: { parent: parent } + } ] end end describe 'hierarchy widget' do context 'when parent is valid work item' do - let_it_be(:parent) { create(:work_item, project: project) } - - let(:widget_params) { { hierarchy_widget: { parent_id: parent.id } } } + let(:widget_params) { { hierarchy_widget: { parent: parent } } } let(:opts) do { @@ -149,7 +152,7 @@ RSpec.describe WorkItems::CreateService do end end - context 'when hiearchy feature flag is disabled' do + context 'when hierarchy feature flag is disabled' do before do stub_feature_flags(work_items_hierarchy: false) end diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb new file mode 100644 index 00000000000..4c155909ac4 --- /dev/null +++ b/spec/services/work_items/parent_links/destroy_service_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ParentLinks::DestroyService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)} + + let(:parent_link_class) { WorkItems::ParentLink } + + subject { described_class.new(parent_link, user).execute } + + context 'when user has permissions to update work items' do + before do + project.add_developer(user) + end + + it 'removes relation' do + expect { subject }.to change(parent_link_class, :count).by(-1) + end + + it 'returns success message' do + is_expected.to eq(message: 'Relation was removed', status: :success) + end + end + + context 'when user has insufficient permissions' do + it 'does not remove relation' do + expect { subject }.not_to change(parent_link_class, :count).from(1) + end + + it 'returns error message' do + is_expected.to eq(message: 'No Work Item Link found', status: :error, http_status: 404) + end + end + end +end diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index 247d10fe245..25b8caba0e8 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe WorkItems::UpdateService do let_it_be(:developer) { create(:user) } let_it_be(:project) { create(:project).tap { |proj| proj.add_developer(developer) } } + let_it_be(:parent) { create(:work_item, project: project) } let_it_be_with_reload(:work_item) { create(:work_item, project: project, assignees: [developer]) } let(:spam_params) { double } @@ -80,7 +81,7 @@ RSpec.describe WorkItems::UpdateService do it_behaves_like 'work item widgetable service' do let(:widget_params) do { - hierarchy_widget: { parent_id: 1 }, + hierarchy_widget: { parent: parent }, description_widget: { description: 'foo' }, weight_widget: { weight: 1 } } @@ -102,7 +103,7 @@ RSpec.describe WorkItems::UpdateService do [ { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } }, { klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } }, - { klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent_id: 1 } } + { klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } } ] end end @@ -144,6 +145,7 @@ RSpec.describe WorkItems::UpdateService do end context 'for the hierarchy widget' do + let(:opts) { { title: 'changed' } } let_it_be(:child_work_item) { create(:work_item, :task, project: project) } let(:widget_params) { { hierarchy_widget: { children_ids: [child_work_item.id] } } } @@ -156,6 +158,23 @@ RSpec.describe WorkItems::UpdateService do expect(work_item.work_item_children).to include(child_work_item) end + + context 'when child type is invalid' do + let_it_be(:child_work_item) { create(:work_item, project: project) } + + it 'returns error status' do + expect(subject[:status]).to be(:error) + expect(subject[:message]) + .to match("#{child_work_item.to_reference} cannot be added: Only Task can be assigned as a child in hierarchy.") + end + + it 'does not update work item attributes' do + expect do + update_work_item + work_item.reload + end.to not_change(WorkItems::ParentLink, :count).and(not_change(work_item, :title)) + end + end end end end diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb index 4765f185a56..528e20f95ad 100644 --- a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb +++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb @@ -21,8 +21,8 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do describe '#update' do subject { described_class.new(widget: widget, current_user: user).before_update_in_transaction(params: params) } - context 'when parent_id and children_ids params are present' do - let(:params) { { parent_id: parent_work_item.id, children_ids: [child_work_item.id] } } + context 'when parent and children_ids params are present' do + let(:params) { { parent: parent_work_item, children_ids: [child_work_item.id] } } it_behaves_like 'raises a WidgetError' do let(:message) { 'A Work Item can be a parent or a child, but not both.' } @@ -95,7 +95,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do context 'when updating parent' do let_it_be(:work_item) { create(:work_item, :task, project: project) } - let(:params) {{ parent_id: parent_work_item.id } } + let(:params) {{ parent: parent_work_item } } context 'when work_items_hierarchy feature flag is disabled' do before do @@ -107,15 +107,6 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end end - context 'when parent_id does not match an existing work item' do - let(:invalid_id) { non_existing_record_iid } - let(:params) {{ parent_id: invalid_id } } - - it_behaves_like 'raises a WidgetError' do - let(:message) { "No Work Item found with ID: #{invalid_id}." } - end - end - context 'when user has insufficient permissions to link work items' do it_behaves_like 'raises a WidgetError' do let(:message) { not_found_error } @@ -127,16 +118,35 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do project.add_developer(user) end - it 'correctly sets work item parent' do - subject - + it 'correctly sets new parent' do + expect(subject[:status]).to eq(:success) expect(work_item.work_item_parent).to eq(parent_work_item) end + context 'when parent is nil' do + let(:params) { { parent: nil } } + + it 'removes the work item parent if present' do + work_item.update!(work_item_parent: parent_work_item) + + expect do + subject + work_item.reload + end.to change(work_item, :work_item_parent).from(parent_work_item).to(nil) + end + + it 'returns success status if parent not present', :aggregate_failure do + work_item.update!(work_item_parent: nil) + + expect(subject[:status]).to eq(:success) + expect(work_item.reload.work_item_parent).to be_nil + end + end + context 'when type is invalid' do let_it_be(:parent_task) { create(:work_item, :task, project: project)} - let(:params) {{ parent_id: parent_task.id } } + let(:params) {{ parent: parent_task } } it_behaves_like 'raises a WidgetError' do let(:message) { "#{work_item.to_reference} cannot be added: Only Issue can be parent of Task." } diff --git a/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb b/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb new file mode 100644 index 00000000000..a62cffc0e1b --- /dev/null +++ b/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +shared_examples_for 'update feature flag client' do + let!(:client) { create(:operations_feature_flags_client, project: project) } + + it 'updates last feature flag updated at' do + freeze_time do + expect { subject }.to change { client.reload.last_feature_flag_updated_at }.from(nil).to(Time.current) + end + end +end + +shared_examples_for 'does not update feature flag client' do + let!(:client) { create(:operations_feature_flags_client, project: project) } + + it 'does not update last feature flag updated at' do + expect { subject }.not_to change { client.reload.last_feature_flag_updated_at } + end +end |