diff options
author | Douglas Barbosa Alexandre <dbalexandre@gmail.com> | 2019-07-09 14:45:46 -0300 |
---|---|---|
committer | Douglas Barbosa Alexandre <dbalexandre@gmail.com> | 2019-07-09 14:45:46 -0300 |
commit | 2615265ef82e07ea36df67f6d1a11ad4a731ad63 (patch) | |
tree | 92c1d3ab69f88e1dceed11ae886e1acb6ae65f1e /spec | |
parent | 203ef336392befd907c335a4a851ec0794d5e91a (diff) | |
parent | 48c19d9e296b2bec3663f46b8d936da2082171b1 (diff) | |
download | gitlab-ce-2615265ef82e07ea36df67f6d1a11ad4a731ad63.tar.gz |
Merge branch 'master' into sathieu/gitlab-ce-project_api
Diffstat (limited to 'spec')
109 files changed, 4862 insertions, 3112 deletions
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index ea68eae12ed..6591901a9dc 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -11,8 +11,10 @@ describe Dashboard::ProjectsController do end context 'user logged in' do + let(:user) { create(:user) } + before do - sign_in create(:user) + sign_in(user) end context 'external authorization' do @@ -24,6 +26,20 @@ describe Dashboard::ProjectsController do expect(response).to have_gitlab_http_status(200) end end + + it 'orders the projects by last activity by default' do + project = create(:project) + project.add_developer(user) + project.update!(last_repository_updated_at: 3.days.ago, last_activity_at: 3.days.ago) + + project2 = create(:project) + project2.add_developer(user) + project2.update!(last_repository_updated_at: 10.days.ago, last_activity_at: 10.days.ago) + + get :index + + expect(assigns(:projects)).to eq([project, project2]) + end end end diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb index 95417936df4..b9ee69a617b 100644 --- a/spec/controllers/projects/deployments_controller_spec.rb +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -41,34 +41,26 @@ describe Projects::DeploymentsController do describe 'GET #metrics' do let(:deployment) { create(:deployment, :success, project: project, environment: environment) } - before do - allow(controller).to receive(:deployment).and_return(deployment) - end - context 'when metrics are disabled' do - before do - allow(deployment).to receive(:has_metrics?).and_return false - end - it 'responds with not found' do - get :metrics, params: deployment_params(id: deployment.id) + get :metrics, params: deployment_params(id: deployment.to_param) expect(response).to be_not_found end end context 'when metrics are enabled' do - before do - allow(deployment).to receive(:has_metrics?).and_return true - end - context 'when environment has no metrics' do before do - expect(deployment).to receive(:metrics).and_return(nil) + expect_next_instance_of(DeploymentMetrics) do |deployment_metrics| + allow(deployment_metrics).to receive(:has_metrics?).and_return(true) + + expect(deployment_metrics).to receive(:metrics).and_return(nil) + end end it 'returns a empty response 204 resposne' do - get :metrics, params: deployment_params(id: deployment.id) + get :metrics, params: deployment_params(id: deployment.to_param) expect(response).to have_gitlab_http_status(204) expect(response.body).to eq('') end @@ -84,11 +76,15 @@ describe Projects::DeploymentsController do end before do - expect(deployment).to receive(:metrics).and_return(empty_metrics) + expect_next_instance_of(DeploymentMetrics) do |deployment_metrics| + allow(deployment_metrics).to receive(:has_metrics?).and_return(true) + + expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics) + end end it 'returns a metrics JSON document' do - get :metrics, params: deployment_params(id: deployment.id) + get :metrics, params: deployment_params(id: deployment.to_param) expect(response).to be_ok expect(json_response['success']).to be(true) @@ -96,54 +92,32 @@ describe Projects::DeploymentsController do expect(json_response['last_update']).to eq(42) end end - - context 'when metrics service does not implement deployment metrics' do - before do - allow(deployment).to receive(:metrics).and_raise(NotImplementedError) - end - - it 'responds with not found' do - get :metrics, params: deployment_params(id: deployment.id) - - expect(response).to be_not_found - end - end end end describe 'GET #additional_metrics' do let(:deployment) { create(:deployment, :success, project: project, environment: environment) } - before do - allow(controller).to receive(:deployment).and_return(deployment) - end - context 'when metrics are disabled' do - before do - allow(deployment).to receive(:has_metrics?).and_return false - end - it 'responds with not found' do - get :metrics, params: deployment_params(id: deployment.id) + get :metrics, params: deployment_params(id: deployment.to_param) expect(response).to be_not_found end end context 'when metrics are enabled' do - let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } - - before do - allow(deployment).to receive(:prometheus_adapter).and_return(prometheus_adapter) - end - context 'when environment has no metrics' do before do - expect(deployment).to receive(:additional_metrics).and_return({}) + expect_next_instance_of(DeploymentMetrics) do |deployment_metrics| + allow(deployment_metrics).to receive(:has_metrics?).and_return(true) + + expect(deployment_metrics).to receive(:additional_metrics).and_return({}) + end end it 'returns a empty response 204 response' do - get :additional_metrics, params: deployment_params(id: deployment.id, format: :json) + get :additional_metrics, params: deployment_params(id: deployment.to_param, format: :json) expect(response).to have_gitlab_http_status(204) expect(response.body).to eq('') end @@ -159,11 +133,15 @@ describe Projects::DeploymentsController do end before do - expect(deployment).to receive(:additional_metrics).and_return(empty_metrics) + expect_next_instance_of(DeploymentMetrics) do |deployment_metrics| + allow(deployment_metrics).to receive(:has_metrics?).and_return(true) + + expect(deployment_metrics).to receive(:additional_metrics).and_return(empty_metrics) + end end it 'returns a metrics JSON document' do - get :additional_metrics, params: deployment_params(id: deployment.id, format: :json) + get :additional_metrics, params: deployment_params(id: deployment.to_param, format: :json) expect(response).to be_ok expect(json_response['success']).to be(true) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 0eca663a683..9878f88a395 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -878,6 +878,22 @@ describe Projects::MergeRequestsController do expect(control_count).to be <= 137 end + it 'has no N+1 issues for environments', :request_store, retry: 0 do + # First run to insert test data from lets, which does take up some 30 queries + get_ci_environments_status + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { get_ci_environments_status }.count + + environment2 = create(:environment, project: forked) + create(:deployment, :succeed, environment: environment2, sha: sha, ref: 'master', deployable: build) + + # TODO address the last 11 queries + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63952 (5 queries) + # And https://gitlab.com/gitlab-org/gitlab-ce/issues/64105 (6 queries) + leeway = 11 + expect { get_ci_environments_status }.not_to exceed_all_query_limit(control_count + leeway) + end + def get_ci_environments_status(extra_params = {}) params = { namespace_id: merge_request.project.namespace.to_param, diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 8d2412f97ef..4e1cac67d23 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -318,6 +318,53 @@ describe ProjectsController do end end + describe '#housekeeping' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:housekeeping) { Projects::HousekeepingService.new(project) } + + context 'when authenticated as owner' do + before do + group.add_owner(user) + sign_in(user) + + allow(Projects::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping) + end + + it 'forces a full garbage collection' do + expect(housekeeping).to receive(:execute).once + + post :housekeeping, + params: { + namespace_id: project.namespace.path, + id: project.path + } + + expect(response).to have_gitlab_http_status(302) + end + end + + context 'when authenticated as developer' do + let(:developer) { create(:user) } + + before do + group.add_developer(developer) + end + + it 'does not execute housekeeping' do + expect(housekeeping).not_to receive(:execute) + + post :housekeeping, + params: { + namespace_id: project.namespace.path, + id: project.path + } + + expect(response).to have_gitlab_http_status(302) + end + end + end + describe "#update" do render_views diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb index b5323a1c76d..ecd0aab925b 100644 --- a/spec/features/admin/admin_sees_project_statistics_spec.rb +++ b/spec/features/admin/admin_sees_project_statistics_spec.rb @@ -15,7 +15,7 @@ describe "Admin > Admin sees project statistics" do let(:project) { create(:project, :repository) } it "shows project statistics" do - expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes wikis, 0 Bytes build artifacts, 0 Bytes LFS)") + expect(page).to have_content("Storage: 0 Bytes (Repository: 0 Bytes / Wikis: 0 Bytes / Build Artifacts: 0 Bytes / LFS: 0 Bytes)") end end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index 55f5ff04d01..254bb12573c 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -22,7 +22,7 @@ describe 'Dashboard shortcuts', :js do find('body').send_keys([:shift, 'T']) - check_page_title('Todos') + check_page_title('To-Do List') find('body').send_keys([:shift, 'P']) diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index d58e3b2841e..c48229fc0a0 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -13,7 +13,7 @@ describe 'Dashboard Todos' do end it 'shows "All done" message' do - expect(page).to have_content 'Todos let you see what you should do next' + expect(page).to have_content 'Your To-Do List shows what to work on next' end end @@ -72,7 +72,7 @@ describe 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'Todos 0' + expect(page).to have_content 'To Do 0' expect(page).to have_content 'Done 1' end @@ -101,7 +101,7 @@ describe 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'Todos 1' + expect(page).to have_content 'To Do 1' expect(page).to have_content 'Done 0' end end @@ -211,7 +211,7 @@ describe 'Dashboard Todos' do describe 'restoring the todo' do before do within first('.todo') do - click_link 'Add todo' + click_link 'Add a To Do' end end @@ -220,7 +220,7 @@ describe 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'Todos 1' + expect(page).to have_content 'To Do 1' expect(page).to have_content 'Done 0' end end @@ -276,7 +276,7 @@ describe 'Dashboard Todos' do end it 'shows "All done" message!' do - expect(page).to have_content 'Todos 0' + expect(page).to have_content 'To Do 0' expect(page).to have_content "You're all done!" expect(page).not_to have_selector('.gl-pagination') end @@ -303,7 +303,7 @@ describe 'Dashboard Todos' do it 'updates todo count' do mark_all_and_undo - expect(page).to have_content 'Todos 2' + expect(page).to have_content 'To Do 2' expect(page).to have_content 'Done 0' end diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 0114178b9be..07ae159eef4 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -13,8 +13,8 @@ describe 'Manually create a todo item from issue', :js do it 'creates todo when clicking button' do page.within '.issuable-sidebar' do - click_button 'Add todo' - expect(page).to have_content 'Mark todo as done' + click_button 'Add a To Do' + expect(page).to have_content 'Mark as done' end page.within '.header-content .todos-count' do @@ -30,8 +30,8 @@ describe 'Manually create a todo item from issue', :js do it 'marks a todo as done' do page.within '.issuable-sidebar' do - click_button 'Add todo' - click_button 'Mark todo as done' + click_button 'Add a To Do' + click_button 'Mark as done' end expect(page).to have_selector('.todos-count', visible: false) diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb new file mode 100644 index 00000000000..7ae4af4667b --- /dev/null +++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe 'User creates confidential merge request on issue page', :js do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + let(:issue) { create(:issue, project: project, confidential: true) } + + def visit_confidential_issue + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + before do + project.add_developer(user) + end + + context 'user has no private fork' do + before do + fork_project(project, user, repository: true) + visit_confidential_issue + end + + it 'shows that user has no fork available' do + click_button 'Create confidential merge request' + + page.within '.create-confidential-merge-request-dropdown-menu' do + expect(page).to have_content('No forks available to you') + end + end + end + + describe 'user has private fork' do + let(:forked_project) { fork_project(project, user, repository: true) } + + before do + forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE) + visit_confidential_issue + end + + it 'create merge request in fork' do + click_button 'Create confidential merge request' + + page.within '.create-confidential-merge-request-dropdown-menu' do + expect(page).to have_button(forked_project.name_with_namespace) + click_button 'Create confidential merge request' + end + + expect(page).to have_content(forked_project.namespace.name) + end + end +end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 733e8aa3eba..30e30751693 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -519,6 +519,8 @@ describe 'Merge request > User sees merge widget', :js do end before do + allow_any_instance_of(TestSuiteComparerEntity) + .to receive(:max_tests).and_return(2) allow_any_instance_of(MergeRequest) .to receive(:has_test_reports?).and_return(true) allow_any_instance_of(MergeRequest) @@ -551,7 +553,7 @@ describe 'Merge request > User sees merge widget', :js do expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('junit found 1 failed test result out of 1 total test') expect(page).to have_content('New') - expect(page).to have_content('subtractTest') + expect(page).to have_content('addTest') end end end @@ -562,7 +564,7 @@ describe 'Merge request > User sees merge widget', :js do click_button 'Expand' within(".js-report-section-container") do - click_button 'subtractTest' + click_button 'addTest' expect(page).to have_content('6.66') expect(page).to have_content(sample_java_failed_message.gsub!(/\s+/, ' ').strip) @@ -596,7 +598,7 @@ describe 'Merge request > User sees merge widget', :js do expect(page).to have_content('rspec found 1 failed test result out of 1 total test') expect(page).to have_content('junit found no changed test results out of 1 total test') expect(page).not_to have_content('New') - expect(page).to have_content('Test#sum when a is 2 and b is 2 returns summary') + expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary') end end end @@ -607,7 +609,7 @@ describe 'Merge request > User sees merge widget', :js do click_button 'Expand' within(".js-report-section-container") do - click_button 'Test#sum when a is 2 and b is 2 returns summary' + click_button 'Test#sum when a is 1 and b is 3 returns summary' expect(page).to have_content('2.22') expect(page).to have_content(sample_rspec_failed_message.gsub!(/\s+/, ' ').strip) @@ -628,13 +630,7 @@ describe 'Merge request > User sees merge widget', :js do let(:head_reports) do Gitlab::Ci::Reports::TestReports.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - reports.get_suite('junit').add_test_case(create_test_case_java_resolved) - end - end - - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + reports.get_suite('junit').add_test_case(create_test_case_java_success) end end @@ -646,7 +642,7 @@ describe 'Merge request > User sees merge widget', :js do within(".js-report-section-container") do expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('junit found 1 fixed test result out of 1 total test') - expect(page).to have_content('subtractTest') + expect(page).to have_content('addTest') end end end @@ -657,15 +653,53 @@ describe 'Merge request > User sees merge widget', :js do click_button 'Expand' within(".js-report-section-container") do - click_button 'subtractTest' + click_button 'addTest' - expect(page).to have_content('6.66') + expect(page).to have_content('5.55') end end end end end + context 'properly truncates the report' do + let(:base_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + 10.times do |index| + reports.get_suite('rspec').add_test_case( + create_test_case_rspec_failed(index)) + reports.get_suite('junit').add_test_case( + create_test_case_java_success(index)) + end + end + end + + let(:head_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + 10.times do |index| + reports.get_suite('rspec').add_test_case( + create_test_case_rspec_failed(index)) + reports.get_suite('junit').add_test_case( + create_test_case_java_failed(index)) + end + end + end + + it 'shows test reports summary which includes the resolved failure' do + within(".js-reports-container") do + click_button 'Expand' + + expect(page).to have_content('Test summary contained 20 failed test results out of 20 total tests') + within(".js-report-section-container") do + expect(page).to have_content('rspec found 10 failed test results out of 10 total tests') + expect(page).to have_content('junit found 10 failed test results out of 10 total tests') + + expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2) + end + end + end + end + def comparer Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) end diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 5ebfc32952d..86331728f88 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -16,16 +16,8 @@ describe 'OAuth Login', :js, :allow_forgery_protection do providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, :facebook, :cas3, :auth0, :authentiq, :salesforce] - before(:all) do - # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` - # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and - # anything after it) from the request URI. - @omniauth_config_full_host = OmniAuth.config.full_host - OmniAuth.config.full_host = ->(request) { request['REQUEST_URI'].sub(/#{request['REQUEST_PATH']}.*/, '') } - end - - after(:all) do - OmniAuth.config.full_host = @omniauth_config_full_host + around(:all) do |example| + with_omniauth_full_host { example.run } end def login_with_provider(provider, enter_two_factor: false) 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 1cc47cd6bd1..7ddd5c12cdf 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'User uses header search field' do +describe 'User uses header search field', :js do include FilteredSearchHelpers let(:project) { create(:project) } @@ -11,57 +11,12 @@ describe 'User uses header search field' do sign_in(user) end - context 'when user is in a global scope', :js do + shared_examples 'search field examples' do before do - visit(root_path) - page.find('#search').click + visit(url) end - context 'when clicking issues' do - it 'shows assigned issues' do - find('.search-input-container .dropdown-menu').click_link('Issues assigned to me') - - expect(page).to have_selector('.filtered-search') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'shows created issues' do - find('.search-input-container .dropdown-menu').click_link("Issues I've created") - - expect(page).to have_selector('.filtered-search') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - end - - context 'when clicking merge requests' do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } - - it 'shows assigned merge requests' do - find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me') - - expect(page).to have_selector('.filtered-search') - expect_tokens([assignee_token(user.name)]) - expect_filtered_search_input_empty - end - - it 'shows created merge requests' do - find('.search-input-container .dropdown-menu').click_link("Merge requests I've created") - - expect(page).to have_selector('.filtered-search') - expect_tokens([author_token(user.name)]) - expect_filtered_search_input_empty - end - end - end - - context 'when user is in a project scope' do - before do - visit(project_path(project)) - end - - it 'starts searching by pressing the enter key', :js do + it 'starts searching by pressing the enter key' do fill_in('search', with: 'gitlab') find('#search').native.send_keys(:enter) @@ -70,30 +25,31 @@ describe 'User uses header search field' do end end - context 'when clicking the search field', :js do + context 'when clicking the search field' do before do page.find('#search').click + wait_for_all_requests end it 'shows category search dropdown' do - expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i) + expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i) end context 'when clicking issues' do let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } it 'shows assigned issues' do - find('.dropdown-menu').click_link('Issues assigned to me') + find('.search-input-container .dropdown-menu').click_link('Issues assigned to me') - expect(page).to have_selector('.filtered-search') + expect(page).to have_selector('.issues-list .issue') expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end it 'shows created issues' do - find('.dropdown-menu').click_link("Issues I've created") + find('.search-input-container .dropdown-menu').click_link("Issues I've created") - expect(page).to have_selector('.filtered-search') + expect(page).to have_selector('.issues-list .issue') expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end @@ -103,33 +59,77 @@ describe 'User uses header search field' do let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } it 'shows assigned merge requests' do - find('.dropdown-menu').click_link('Merge requests assigned to me') + find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me') - expect(page).to have_selector('.merge-requests-holder') + expect(page).to have_selector('.mr-list .merge-request') expect_tokens([assignee_token(user.name)]) expect_filtered_search_input_empty end it 'shows created merge requests' do - find('.dropdown-menu').click_link("Merge requests I've created") + find('.search-input-container .dropdown-menu').click_link("Merge requests I've created") - expect(page).to have_selector('.merge-requests-holder') + expect(page).to have_selector('.mr-list .merge-request') expect_tokens([author_token(user.name)]) expect_filtered_search_input_empty end end end - context 'when entering text into the search field', :js do + context 'when entering text into the search field' do before do page.within('.search-input-wrap') do - fill_in('search', with: project.name[0..3]) + fill_in('search', with: scope_name.first(4)) end end it 'does not display the category search dropdown' do - expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i) + expect(page).not_to have_selector('.dropdown-header', text: /#{scope_name}/i) end end end + + context 'when user is in a global scope' do + include_examples 'search field examples' do + let(:url) { root_path } + let(:scope_name) { 'All GitLab' } + end + end + + context 'when user is in a project scope' do + include_examples 'search field examples' do + let(:url) { project_path(project) } + let(:scope_name) { project.name } + end + end + + context 'when user is in a group scope' do + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + + before do + group.add_maintainer(user) + end + + include_examples 'search field examples' do + let(:url) { group_path(group) } + let(:scope_name) { group.name } + end + end + + context 'when user is in a subgroup scope' do + let(:group) { create(:group) } + let(:subgroup) { create(:group, :public, parent: group) } + let(:project) { create(:project, namespace: subgroup) } + + before do + group.add_owner(user) + subgroup.add_owner(user) + end + + include_examples 'search field examples' do + let(:url) { group_path(subgroup) } + let(:scope_name) { subgroup.name } + end + end end diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 97304170c4e..01f45a37ba8 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -35,5 +35,27 @@ describe RunnerJobsFinder do end end end + + context 'when order_by and sort are specified' do + context 'when order_by id and sort is asc' do + let(:params) { { order_by: 'id', sort: 'asc' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } + + it 'sorts as id: :asc' do + is_expected.to eq(jobs.sort_by(&:id)) + end + end + end + + context 'when order_by is specified and sort is not specified' do + context 'when order_by id and sort is not specified' do + let(:params) { { order_by: 'id' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } + + it 'sorts as id: :desc' do + is_expected.to eq(jobs.sort_by(&:id).reverse) + end + end + end end end diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 0188d12a57d..7004373be0e 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -412,6 +412,22 @@ describe('Api', () => { }); }); + describe('user counts', () => { + it('fetches single user counts', done => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/user_counts`; + mock.onGet(expectedUrl).reply(200, { + merge_requests: 4, + }); + + Api.userCounts() + .then(({ data }) => { + expect(data.merge_requests).toBe(4); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('user status', () => { it('fetches single user status', done => { const userId = '123456'; diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js index de9fc998360..a8a322e7237 100644 --- a/spec/frontend/boards/services/board_service_spec.js +++ b/spec/frontend/boards/services/board_service_spec.js @@ -2,6 +2,7 @@ import BoardService from '~/boards/services/board_service'; import { TEST_HOST } from 'helpers/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; +import boardsStore from '~/boards/stores/boards_store'; describe('BoardService', () => { const dummyResponse = "without type checking this doesn't matter"; @@ -18,10 +19,11 @@ describe('BoardService', () => { beforeEach(() => { axiosMock = new AxiosMockAdapter(axios); - service = new BoardService({ + boardsStore.setEndpoints({ ...endpoints, boardId, }); + service = new BoardService(); }); describe('all', () => { diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js index 4ed77c3a036..8283bc966e4 100644 --- a/spec/frontend/branches/divergence_graph_spec.js +++ b/spec/frontend/branches/divergence_graph_spec.js @@ -10,12 +10,14 @@ describe('Divergence graph', () => { mock.onGet('/-/diverging_counts').reply(200, { master: { ahead: 1, behind: 1 }, + 'test/hello-world': { ahead: 1, behind: 1 }, }); jest.spyOn(axios, 'get'); document.body.innerHTML = ` - <div class="js-branch-item" data-name="master"></div> + <div class="js-branch-item" data-name="master"><div class="js-branch-divergence-graph"></div></div> + <div class="js-branch-item" data-name="test/hello-world"><div class="js-branch-divergence-graph"></div></div> `; }); @@ -26,7 +28,13 @@ describe('Divergence graph', () => { it('calls axos get with list of branch names', () => init('/-/diverging_counts').then(() => { expect(axios.get).toHaveBeenCalledWith('/-/diverging_counts', { - params: { names: ['master'] }, + params: { names: ['master', 'test/hello-world'] }, }); })); + + it('creates Vue components', () => + init('/-/diverging_counts').then(() => { + expect(document.querySelector('[data-name="master"]').innerHTML).not.toEqual(''); + expect(document.querySelector('[data-name="test/hello-world"]').innerHTML).not.toEqual(''); + })); }); diff --git a/spec/frontend/commons/nav/user_merge_requests_spec.js b/spec/frontend/commons/nav/user_merge_requests_spec.js new file mode 100644 index 00000000000..4da6d53557a --- /dev/null +++ b/spec/frontend/commons/nav/user_merge_requests_spec.js @@ -0,0 +1,113 @@ +import { + openUserCountsBroadcast, + closeUserCountsBroadcast, + refreshUserMergeRequestCounts, +} from '~/commons/nav/user_merge_requests'; +import Api from '~/api'; + +jest.mock('~/api'); + +const TEST_COUNT = 1000; +const MR_COUNT_CLASS = 'merge-requests-count'; + +describe('User Merge Requests', () => { + let channelMock; + let newBroadcastChannelMock; + + beforeEach(() => { + global.gon.current_user_id = 123; + + channelMock = { + postMessage: jest.fn(), + close: jest.fn(), + }; + newBroadcastChannelMock = jest.fn().mockImplementation(() => channelMock); + + global.BroadcastChannel = newBroadcastChannelMock; + setFixtures(`<div class="${MR_COUNT_CLASS}">0</div>`); + }); + + const findMRCountText = () => document.body.querySelector(`.${MR_COUNT_CLASS}`).textContent; + + describe('refreshUserMergeRequestCounts', () => { + beforeEach(() => { + Api.userCounts.mockReturnValue( + Promise.resolve({ + data: { merge_requests: TEST_COUNT }, + }), + ); + }); + + describe('with open broadcast channel', () => { + beforeEach(() => { + openUserCountsBroadcast(); + + return refreshUserMergeRequestCounts(); + }); + + it('updates the top count of merge requests', () => { + expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString()); + }); + + it('calls the API', () => { + expect(Api.userCounts).toHaveBeenCalled(); + }); + + it('posts count to BroadcastChannel', () => { + expect(channelMock.postMessage).toHaveBeenCalledWith(TEST_COUNT); + }); + }); + + describe('without open broadcast channel', () => { + beforeEach(() => refreshUserMergeRequestCounts()); + + it('does not post anything', () => { + expect(channelMock.postMessage).not.toHaveBeenCalled(); + }); + }); + }); + + describe('openUserCountsBroadcast', () => { + beforeEach(() => { + openUserCountsBroadcast(); + }); + + it('creates BroadcastChannel that updates DOM on message received', () => { + expect(findMRCountText()).toEqual('0'); + + channelMock.onmessage({ data: TEST_COUNT }); + + expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString()); + }); + + it('closes if called while already open', () => { + expect(channelMock.close).not.toHaveBeenCalled(); + + openUserCountsBroadcast(); + + expect(channelMock.close).toHaveBeenCalled(); + }); + }); + + describe('closeUserCountsBroadcast', () => { + describe('when not opened', () => { + it('does nothing', () => { + expect(channelMock.close).not.toHaveBeenCalled(); + }); + }); + + describe('when opened', () => { + beforeEach(() => { + openUserCountsBroadcast(); + }); + + it('closes', () => { + expect(channelMock.close).not.toHaveBeenCalled(); + + closeUserCountsBroadcast(); + + expect(channelMock.close).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap new file mode 100644 index 00000000000..fd307ce5ab3 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = ` +<div + class="form-group" +> + <label> + Project + </label> + + <div> + <!----> + + <p + class="text-muted mt-1 mb-0" + > + + No forks available to you. + <br /> + + <span> + To protect this issue's confidentiality, + <a + class="help-link" + href="https://test.com" + > + fork the project + </a> + and set the forks visiblity to private. + </span> + </p> + </div> +</div> +`; + +exports[`Confidential merge request project form group component renders fork dropdown 1`] = ` +<div + class="form-group" +> + <label> + Project + </label> + + <div> + <!----> + + <p + class="text-muted mt-1 mb-0" + > + + No forks available to you. + <br /> + + <span> + To protect this issue's confidentiality, + <a + class="help-link" + href="https://test.com" + > + fork the project + </a> + and set the forks visiblity to private. + </span> + </p> + </div> +</div> +`; diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js new file mode 100644 index 00000000000..69495f3c161 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js @@ -0,0 +1,56 @@ +import { mount } from '@vue/test-utils'; +import { GlDropdownItem } from '@gitlab/ui'; +import Dropdown from '~/confidential_merge_request/components/dropdown.vue'; + +let vm; + +function factory(projects = []) { + vm = mount(Dropdown, { + propsData: { + projects, + selectedProject: projects[0], + }, + }); +} + +describe('Confidential merge request project dropdown component', () => { + afterEach(() => { + vm.destroy(); + }); + + it('renders dropdown items', () => { + factory([ + { + id: 1, + name: 'test', + }, + { + id: 2, + name: 'test', + }, + ]); + + expect(vm.findAll(GlDropdownItem).length).toBe(2); + }); + + it('renders selected project icon', () => { + factory([ + { + id: 1, + name: 'test', + }, + { + id: 2, + name: 'test 2', + }, + ]); + + expect(vm.find('.js-active-project-check').classes()).not.toContain('icon'); + expect( + vm + .findAll('.js-active-project-check') + .at(1) + .classes(), + ).toContain('icon'); + }); +}); diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js new file mode 100644 index 00000000000..3001363f7b9 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js @@ -0,0 +1,77 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue'; + +const localVue = createLocalVue(); +const mockData = [ + { + id: 1, + name_with_namespace: 'root / gitlab-ce', + path_with_namespace: 'root/gitlab-ce', + namespace: { + full_path: 'root', + }, + }, + { + id: 2, + name_with_namespace: 'test / gitlab-ce', + path_with_namespace: 'test/gitlab-ce', + namespace: { + full_path: 'test', + }, + }, +]; +let vm; +let mock; + +function factory(projects = mockData) { + mock = new MockAdapter(axios); + mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects); + + vm = shallowMount(ProjectFormGroup, { + localVue, + propsData: { + namespacePath: 'gitlab-org', + projectPath: 'gitlab-org/gitlab-ce', + newForkPath: 'https://test.com', + helpPagePath: '/help', + }, + }); +} + +describe('Confidential merge request project form group component', () => { + afterEach(() => { + mock.restore(); + vm.destroy(); + }); + + it('renders fork dropdown', () => { + factory(); + + return localVue.nextTick(() => { + expect(vm.element).toMatchSnapshot(); + }); + }); + + it('sets selected project as first fork', () => { + factory(); + + return localVue.nextTick(() => { + expect(vm.vm.selectedProject).toEqual({ + id: 1, + name: 'root / gitlab-ce', + pathWithNamespace: 'root/gitlab-ce', + namespaceFullpath: 'root', + }); + }); + }); + + it('renders empty state when response is empty', () => { + factory([]); + + return localVue.nextTick(() => { + expect(vm.element).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js index 00fe3f451f5..6e41fdabdce 100644 --- a/spec/javascripts/create_merge_request_dropdown_spec.js +++ b/spec/frontend/create_merge_request_dropdown_spec.js @@ -1,7 +1,7 @@ import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; import CreateMergeRequestDropdown from '~/create_merge_request_dropdown'; -import { TEST_HOST } from 'spec/test_constants'; +import { TEST_HOST } from './helpers/test_constants'; describe('CreateMergeRequestDropdown', () => { let axiosMock; @@ -10,7 +10,7 @@ describe('CreateMergeRequestDropdown', () => { beforeEach(() => { axiosMock = new MockAdapter(axios); - setFixtures(` + document.body.innerHTML = ` <div id="dummy-wrapper-element"> <div class="available"></div> <div class="unavailable"> @@ -18,11 +18,12 @@ describe('CreateMergeRequestDropdown', () => { <div class="text"></div> </div> <div class="js-ref"></div> + <div class="js-create-mr"></div> <div class="js-create-merge-request"></div> <div class="js-create-target"></div> <div class="js-dropdown-toggle"></div> </div> - `); + `; const dummyElement = document.getElementById('dummy-wrapper-element'); dropdown = new CreateMergeRequestDropdown(dummyElement); @@ -36,7 +37,7 @@ describe('CreateMergeRequestDropdown', () => { describe('getRef', () => { it('escapes branch names correctly', done => { const endpoint = `${dropdown.refsPath}contains%23hash`; - spyOn(axios, 'get').and.callThrough(); + jest.spyOn(axios, 'get'); axiosMock.onGet(endpoint).replyOnce({}); dropdown diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js index aa1fa0373db..08a31318544 100644 --- a/spec/frontend/ide/lib/files_spec.js +++ b/spec/frontend/ide/lib/files_spec.js @@ -12,6 +12,7 @@ const createEntries = paths => { const { name, parent } = splitParent(path); const parentEntry = acc[parent]; + const previewMode = viewerInformationForPath(name); acc[path] = { ...decorateData({ @@ -22,7 +23,8 @@ const createEntries = paths => { path, url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${escapeFileUrl(path)}`), type, - previewMode: viewerInformationForPath(path), + previewMode, + binary: (previewMode && previewMode.binary) || false, parentPath: parent, parentTreeUrl: parentEntry ? parentEntry.url diff --git a/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap b/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap new file mode 100644 index 00000000000..5f24bab600c --- /dev/null +++ b/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyState shows gettingStarted state 1`] = ` +<glemptystate-stub + description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments." + primarybuttonlink="/clustersPath" + primarybuttontext="Install on clusters" + secondarybuttonlink="/settingsPath" + secondarybuttontext="Configure existing installation" + svgpath="/path/to/getting-started.svg" + title="Get started with performance monitoring" +/> +`; + +exports[`EmptyState shows loading state 1`] = ` +<glemptystate-stub + description="Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available." + primarybuttonlink="/documentationPath" + primarybuttontext="View documentation" + secondarybuttonlink="" + secondarybuttontext="" + svgpath="/path/to/loading.svg" + title="Waiting for performance data" +/> +`; + +exports[`EmptyState shows unableToConnect state 1`] = ` +<glemptystate-stub + description="Ensure connectivity is available from the GitLab server to the Prometheus server" + primarybuttonlink="/documentationPath" + primarybuttontext="View documentation" + secondarybuttonlink="/settingsPath" + secondarybuttontext="Configure Prometheus" + svgpath="/path/to/unable-to-connect.svg" + title="Unable to connect to Prometheus server" +/> +`; diff --git a/spec/frontend/monitoring/dashboard_state_spec.js b/spec/frontend/monitoring/dashboard_state_spec.js new file mode 100644 index 00000000000..950422911eb --- /dev/null +++ b/spec/frontend/monitoring/dashboard_state_spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import EmptyState from '~/monitoring/components/empty_state.vue'; + +function createComponent(props) { + return shallowMount(EmptyState, { + propsData: { + ...props, + settingsPath: '/settingsPath', + clustersPath: '/clustersPath', + documentationPath: '/documentationPath', + emptyGettingStartedSvgPath: '/path/to/getting-started.svg', + emptyLoadingSvgPath: '/path/to/loading.svg', + emptyNoDataSvgPath: '/path/to/no-data.svg', + emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', + }, + }); +} + +describe('EmptyState', () => { + it('shows gettingStarted state', () => { + const wrapper = createComponent({ + selectedState: 'gettingStarted', + }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows loading state', () => { + const wrapper = createComponent({ + selectedState: 'loading', + }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows unableToConnect state', () => { + const wrapper = createComponent({ + selectedState: 'unableToConnect', + }); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js new file mode 100644 index 00000000000..279ca017b44 --- /dev/null +++ b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js @@ -0,0 +1,51 @@ +import { mount } from '@vue/test-utils'; +import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue'; + +const TEST_CHILDREN = '<li>Hello!</li><li>World!</li>'; + +// We have to wrap our SUT with a TestComponent because multiple roots are possible +// because it's a functional component. +const TestComponent = { + components: { DiscussionNotesRepliesWrapper }, + template: `<ul><discussion-notes-replies-wrapper v-bind="$attrs">${TEST_CHILDREN}</discussion-notes-replies-wrapper></ul>`, +}; + +describe('DiscussionNotesRepliesWrapper', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mount(TestComponent, { + propsData: props, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when normal discussion', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders children directly', () => { + expect(wrapper.html()).toEqual(`<ul>${TEST_CHILDREN}</ul>`); + }); + }); + + describe('when diff discussion', () => { + beforeEach(() => { + createComponent({ + isDiffDiscussion: true, + }); + }); + + it('wraps children with notes', () => { + const notes = wrapper.find('li.discussion-collapsible ul.notes'); + + expect(notes.exists()).toBe(true); + expect(notes.html()).toEqual(`<ul class="notes">${TEST_CHILDREN}</ul>`); + }); + }); +}); diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index d36e428a8ee..93b86b9b812 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -21,6 +21,10 @@ describe GitlabSchema do expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation)) end + it 'enables using gitaly call checker' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::CallsGitaly::Instrumentation)) + end + it 'has the base mutation' do expect(described_class.mutation).to eq(::Types::MutationType.to_graphql) end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index 0d3c3e37daf..77ef8933717 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -22,6 +22,24 @@ describe Types::BaseField do expect(field.to_graphql.complexity).to eq 1 end + describe '#base_complexity' do + context 'with no gitaly calls' do + it 'defaults to 1' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) + + expect(field.base_complexity).to eq 1 + end + end + + context 'with a gitaly call' do + it 'adds 1 to the default value' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.base_complexity).to eq 2 + end + end + end + it 'has specified value' do field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) @@ -52,5 +70,46 @@ describe Types::BaseField do end end end + + context 'calls_gitaly' do + context 'for fields with a resolver' do + it 'adds 1 if true' do + with_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true, calls_gitaly: true) + without_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true) + base_result = without_gitaly_field.to_graphql.complexity.call({}, {}, 2) + + expect(with_gitaly_field.to_graphql.complexity.call({}, {}, 2)).to eq base_result + 1 + end + end + + context 'for fields without a resolver' do + it 'adds 1 if true' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.to_graphql.complexity).to eq 2 + end + end + + it 'defaults to false' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) + + expect(field.base_complexity).to eq Types::BaseField::DEFAULT_COMPLEXITY + end + + context 'with declared constant complexity value' do + it 'has complexity set to that constant' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) + + expect(field.to_graphql.complexity).to eq 12 + end + + it 'does not raise an error even with Gitaly calls' do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return([0, 1]) + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) + + expect(field.to_graphql.complexity).to eq 12 + end + end + end end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index db0d45c3692..554c08add2d 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -28,7 +28,7 @@ describe PreferencesHelper do ["Your Projects' Activity", 'project_activity'], ["Starred Projects' Activity", 'starred_project_activity'], ["Your Groups", 'groups'], - ["Your Todos", 'todos'], + ["Your To-Do List", 'todos'], ["Assigned Issues", 'issues'], ["Assigned Merge Requests", 'merge_requests'] ] diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb index 62c00964524..4bd0fbb76ca 100644 --- a/spec/helpers/storage_helper_spec.rb +++ b/spec/helpers/storage_helper_spec.rb @@ -31,7 +31,7 @@ describe StorageHelper do build_artifacts_size: 30.megabytes)) end - let(:message) { '10 KB repositories, 10 Bytes wikis, 30 MB build artifacts, 20 GB LFS' } + let(:message) { 'Repository: 10 KB / Wikis: 10 Bytes / Build Artifacts: 30 MB / LFS: 20 GB' } it 'works on ProjectStatistics' do expect(helper.storage_counters_details(project.statistics)).to eq(message) diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 9854cf49e97..ea22ae5c4e7 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,4 +1,5 @@ import BoardService from '~/boards/services/board_service'; +import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { id: 1, @@ -76,12 +77,14 @@ export const mockBoardService = (opts = {}) => { const bulkUpdatePath = opts.bulkUpdatePath || ''; const boardId = opts.boardId || '1'; - return new BoardService({ + boardsStore.setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, }); + + return new BoardService(); }; export const mockAssigneesList = [ diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index bb90e53e525..f75d63c8f57 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -58,7 +58,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('sets default tooltip title', () => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }); it('toggle todo state', done => { @@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -99,7 +99,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('data-original-title'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -124,13 +124,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }) .then(done) .catch(done.fail); }); - it('updates aria-label to mark todo as done', done => { + it('updates aria-label to Mark as done', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); setTimeout(() => { @@ -138,7 +138,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -153,7 +153,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) @@ -163,7 +163,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js index 0b3890b68d6..9b61dbe7975 100644 --- a/spec/javascripts/diffs/components/inline_diff_view_spec.js +++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js @@ -10,6 +10,7 @@ describe('InlineDiffView', () => { let component; const getDiffFileMock = () => Object.assign({}, diffFileMockData); const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)]; + const notesLength = getDiscussionsMockData()[0].notes.length; beforeEach(done => { const diffFile = getDiffFileMock(); @@ -40,7 +41,7 @@ describe('InlineDiffView', () => { Vue.nextTick(() => { expect(el.querySelectorAll('.notes_holder').length).toEqual(1); - expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(6); + expect(el.querySelectorAll('.notes_holder .note').length).toEqual(notesLength + 1); expect(el.innerText.indexOf('comment 5')).toBeGreaterThan(-1); component.$store.dispatch('setInitialNotes', []); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index f832096701f..7dc5cb24981 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -30,6 +30,7 @@ describe('RepoEditor', () => { Vue.set(vm.$store.state.entries, f.path, f); spyOn(vm, 'getFileData').and.returnValue(Promise.resolve()); + spyOn(vm, 'getRawFileData').and.returnValue(Promise.resolve()); vm.$mount(); @@ -407,6 +408,44 @@ describe('RepoEditor', () => { }); }); + describe('initEditor', () => { + beforeEach(() => { + spyOn(vm.editor, 'createInstance'); + spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); + }); + + it('is being initialised for files without content even if shouldHideEditor is `true`', done => { + vm.file.content = ''; + vm.file.raw = ''; + + vm.initEditor(); + vm.$nextTick() + .then(() => { + expect(vm.getFileData).toHaveBeenCalled(); + expect(vm.getRawFileData).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('does not initialize editor for files already with content', done => { + expect(vm.getFileData.calls.count()).toEqual(1); + expect(vm.getRawFileData.calls.count()).toEqual(1); + + vm.file.content = 'foo'; + + vm.initEditor(); + vm.$nextTick() + .then(() => { + expect(vm.getFileData.calls.count()).toEqual(1); + expect(vm.getRawFileData.calls.count()).toEqual(1); + expect(vm.editor.createInstance).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + it('calls removePendingTab when old file is pending', done => { spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); spyOn(vm, 'removePendingTab'); @@ -416,6 +455,7 @@ describe('RepoEditor', () => { vm.$nextTick() .then(() => { vm.file = file('testing'); + vm.file.content = 'foo'; // need to prevent full cycle of initEditor return vm.$nextTick(); }) diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 18ee4330f69..7714f66c9a4 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -83,6 +83,26 @@ describe('IDE store file mutations', () => { expect(localFile.raw).toBeNull(); expect(localFile.baseRaw).toBeNull(); }); + + it('sets extra file data to all arrays concerned', () => { + localState.stagedFiles = [localFile]; + localState.changedFiles = [localFile]; + localState.openFiles = [localFile]; + + const rawPath = 'foo/bar/blah.md'; + + mutations.SET_FILE_DATA(localState, { + data: { + raw_path: rawPath, + }, + file: localFile, + }); + + expect(localState.stagedFiles[0].rawPath).toEqual(rawPath); + expect(localState.changedFiles[0].rawPath).toEqual(rawPath); + expect(localState.openFiles[0].rawPath).toEqual(rawPath); + expect(localFile.rawPath).toEqual(rawPath); + }); }); describe('SET_FILE_RAW_DATA', () => { diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 25543053eba..4d57bfb1b33 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -2,14 +2,14 @@ import $ from 'jquery'; import MockAdaptor from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import IssuableIndex from '~/issuable_index'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; describe('Issuable', () => { - let Issuable; describe('initBulkUpdate', () => { it('should not set bulkUpdateSidebar', () => { - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new - expect(Issuable.bulkUpdateSidebar).not.toBeDefined(); + expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeNull(); }); it('should set bulkUpdateSidebar', () => { @@ -17,9 +17,9 @@ describe('Issuable', () => { element.classList.add('issues-bulk-update'); document.body.appendChild(element); - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new - expect(Issuable.bulkUpdateSidebar).toBeDefined(); + expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined(); }); }); @@ -36,7 +36,7 @@ describe('Issuable', () => { input.setAttribute('id', 'issuable_email'); document.body.appendChild(input); - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new mock = new MockAdaptor(axios); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js deleted file mode 100644 index 6b2be83aa8c..00000000000 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import Vue from 'vue'; -import EmptyState from '~/monitoring/components/empty_state.vue'; -import { statePaths } from './mock_data'; - -function createComponent(props) { - const Component = Vue.extend(EmptyState); - - return new Component({ - propsData: { - ...props, - settingsPath: statePaths.settingsPath, - clustersPath: statePaths.clustersPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: '/path/to/getting-started.svg', - emptyLoadingSvgPath: '/path/to/loading.svg', - emptyNoDataSvgPath: '/path/to/no-data.svg', - emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', - }, - }).$mount(); -} - -function getTextFromNode(component, selector) { - return component.$el.querySelector(selector).firstChild.nodeValue.trim(); -} - -describe('EmptyState', () => { - describe('Computed props', () => { - it('currentState', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - }); - - expect(component.currentState).toBe(component.states.gettingStarted); - }); - - it('showButtonDescription returns a description with a link for the unableToConnect state', () => { - const component = createComponent({ - selectedState: 'unableToConnect', - }); - - expect(component.showButtonDescription).toEqual(true); - }); - - it('showButtonDescription returns the description without a link for any other state', () => { - const component = createComponent({ - selectedState: 'loading', - }); - - expect(component.showButtonDescription).toEqual(false); - }); - }); - - it('should show the gettingStarted state', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual( - component.states.gettingStarted.title, - ); - - expect(getTextFromNode(component, '.state-description')).toEqual( - component.states.gettingStarted.description, - ); - - expect(getTextFromNode(component, '.btn-success')).toEqual( - component.states.gettingStarted.buttonText, - ); - }); - - it('should show the loading state', () => { - const component = createComponent({ - selectedState: 'loading', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title); - expect(getTextFromNode(component, '.state-description')).toEqual( - component.states.loading.description, - ); - - expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText); - }); - - it('should show the unableToConnect state', () => { - const component = createComponent({ - selectedState: 'unableToConnect', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual( - component.states.unableToConnect.title, - ); - - expect(component.$el.querySelector('.state-description a')).toBeDefined(); - expect(getTextFromNode(component, '.btn-success')).toEqual( - component.states.unableToConnect.buttonText, - ); - }); -}); diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/javascripts/monitoring/store/utils_spec.js new file mode 100644 index 00000000000..73dd370ffb3 --- /dev/null +++ b/spec/javascripts/monitoring/store/utils_spec.js @@ -0,0 +1,37 @@ +import { groupQueriesByChartInfo } from '~/monitoring/stores/utils'; + +describe('groupQueriesByChartInfo', () => { + let input; + let output; + + it('groups metrics with the same chart title and y_axis label', () => { + input = [ + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'new title', y_label: 'MB', queries: [{}] }, + ]; + + output = [ + { title: 'title', y_label: 'MB', queries: [{ metricId: null }, { metricId: null }] }, + { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] }, + ]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /additional_metrics endpoint + it("associates a chart's stringified metric_id with the metric", () => { + input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }]; + output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /metrics_dashboard endpoint + it('aliases a stringified metrics_id on the metric to the metricId key', () => { + input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }]; + output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); +}); diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 362963ddaf4..88c86746992 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -251,6 +251,21 @@ describe('issue_comment_form component', () => { }); }); }); + + describe('when toggling state', () => { + it('should update MR count', done => { + spyOn(vm, 'closeIssue').and.returnValue(Promise.resolve()); + + const updateMrCountSpy = spyOnDependency(CommentForm, 'refreshUserMergeRequestCounts'); + vm.toggleIssueState(); + + Vue.nextTick(() => { + expect(updateMrCountSpy).toHaveBeenCalled(); + + done(); + }); + }); + }); }); describe('issue is confidential', () => { diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 65f72a135aa..c97ff5236ec 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import $ from 'jquery'; import _ from 'underscore'; +import Api from '~/api'; import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import actionsModule, * as actions from '~/notes/stores/actions'; @@ -8,7 +9,6 @@ import * as mutationTypes from '~/notes/stores/mutation_types'; import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; -import service from '~/notes/services/notes_service'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -846,9 +846,9 @@ describe('Actions Notes Store', () => { let flashContainer; beforeEach(() => { - spyOn(service, 'applySuggestion'); + spyOn(Api, 'applySuggestion'); dispatch.and.returnValue(Promise.resolve()); - service.applySuggestion.and.returnValue(Promise.resolve()); + Api.applySuggestion.and.returnValue(Promise.resolve()); flashContainer = {}; }); @@ -877,7 +877,7 @@ describe('Actions Notes Store', () => { it('when service fails, flashes error message', done => { const response = { response: { data: { message: TEST_ERROR_MESSAGE } } }; - service.applySuggestion.and.returnValue(Promise.reject(response)); + Api.applySuggestion.and.returnValue(Promise.reject(response)); testSubmitSuggestion(done, () => { expect(commit).not.toHaveBeenCalled(); diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js index f46ea5a0499..e7abd19c865 100644 --- a/spec/javascripts/sidebar/todo_spec.js +++ b/spec/javascripts/sidebar/todo_spec.js @@ -53,14 +53,14 @@ describe('SidebarTodo', () => { describe('buttonLabel', () => { it('returns todo button text for marking todo as done when `isTodo` prop is `true`', () => { - expect(vm.buttonLabel).toBe('Mark todo as done'); + expect(vm.buttonLabel).toBe('Mark as done'); }); it('returns todo button text for add todo when `isTodo` prop is `false`', done => { vm.isTodo = false; Vue.nextTick() .then(() => { - expect(vm.buttonLabel).toBe('Add todo'); + expect(vm.buttonLabel).toBe('Add a To Do'); }) .then(done) .catch(done.fail); @@ -131,14 +131,14 @@ describe('SidebarTodo', () => { }); it('check button label computed property', () => { - expect(vm.buttonLabel).toEqual('Mark todo as done'); + expect(vm.buttonLabel).toEqual('Mark as done'); }); it('renders button label element when `collapsed` prop is `false`', () => { const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner'); expect(buttonLabelEl).not.toBeNull(); - expect(buttonLabelEl.innerText.trim()).toBe('Mark todo as done'); + expect(buttonLabelEl.innerText.trim()).toBe('Mark as done'); }); it('renders button icon when `collapsed` prop is `true`', done => { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 2cc476ed52a..50741e249ca 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -8,7 +8,6 @@ import '~/commons'; import Vue from 'vue'; import VueResource from 'vue-resource'; import Translate from '~/vue_shared/translate'; -import CheckEE from '~/vue_shared/mixins/is_ee'; import jasmineDiff from 'jasmine-diff'; import { config as testUtilsConfig } from '@vue/test-utils'; @@ -48,7 +47,6 @@ Vue.config.errorHandler = function(err) { Vue.use(VueResource); Vue.use(Translate); -Vue.use(CheckEE); // enable test fixtures jasmine.getFixtures().fixturesPath = FIXTURES_PATH; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index bb76616be56..ba3ba01944d 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -58,9 +58,11 @@ const createComponent = (customConfig = {}) => { describe('ReadyToMerge', () => { let vm; + let updateMrCountSpy; beforeEach(() => { vm = createComponent(); + updateMrCountSpy = spyOnDependency(ReadyToMerge, 'refreshUserMergeRequestCounts'); }); afterEach(() => { @@ -461,6 +463,7 @@ describe('ReadyToMerge', () => { expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent'); expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled(); + expect(updateMrCountSpy).toHaveBeenCalled(); expect(cpc).toBeFalsy(); expect(spc).toBeTruthy(); diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 403e0785d1b..127463a57e8 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -144,6 +144,68 @@ describe Feature do expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy end + it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) } + it { expect(described_class.l2_cache_backend).to eq(Rails.cache) } + + it 'caches the status in L1 and L2 caches', + :request_store, :use_clean_rails_memory_store_caching do + described_class.enable(:enabled_feature_flag) + flipper_key = "flipper/v1/feature/enabled_feature_flag" + + expect(described_class.l2_cache_backend) + .to receive(:fetch) + .once + .with(flipper_key, expires_in: 1.hour) + .and_call_original + + expect(described_class.l1_cache_backend) + .to receive(:fetch) + .once + .with(flipper_key, expires_in: 1.minute) + .and_call_original + + 2.times do + expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy + end + end + + context 'cached feature flag', :request_store do + let(:flag) { :some_feature_flag } + + before do + described_class.flipper.memoize = false + described_class.enabled?(flag) + end + + it 'caches the status in L1 cache for the first minute' do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.l2_cache_backend).not_to receive(:fetch) + expect(described_class.enabled?(flag)).to be_truthy + end.not_to exceed_query_limit(0) + end + + it 'caches the status in L2 cache after 2 minutes' do + Timecop.travel 2.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.enabled?(flag)).to be_truthy + end.not_to exceed_query_limit(0) + end + end + + it 'fetches the status after an hour' do + Timecop.travel 61.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.enabled?(flag)).to be_truthy + end.not_to exceed_query_limit(1) + end + end + end + context 'with an individual actor' do CustomActor = Struct.new(:flipper_id) diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 483c5ea9cff..972dd7e0d2b 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -26,6 +26,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end it 'loads 10 projects without hitting Gitaly call limit', :request_store do + allow(Gitlab::GitalyClient).to receive(:can_access_disk?).and_return(false) projects = Gitlab::GitalyClient.allow_n_plus_1_calls do (1..10).map { create(:project, :repository) } end diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb index 71c61e0345f..36582204cc1 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb @@ -74,17 +74,11 @@ describe Gitlab::Ci::Reports::TestReportsComparer do subject { comparer.resolved_count } context 'when there is a resolved test case in head suites' do - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end - end - before do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) end it 'returns the correct count' do diff --git a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb index 6ab16e5518d..579b3e6fd24 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb @@ -10,12 +10,6 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do let(:test_case_success) { create_test_case_rspec_success } let(:test_case_failed) { create_test_case_rspec_failed } - let(:test_case_resolved) do - create_test_case_rspec_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end - end - describe '#new_failures' do subject { comparer.new_failures } @@ -44,7 +38,7 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when head sutie has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'does not return the failed test case' do @@ -81,7 +75,7 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when head sutie has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'does not return the failed test case' do @@ -126,11 +120,11 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when head sutie has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'does not return the resolved test case' do - is_expected.to eq([test_case_resolved]) + is_expected.to eq([test_case_success]) end it 'returns the correct resolved count' do @@ -156,13 +150,8 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when there are a new failure and an existing failure' do let(:test_case_1_success) { create_test_case_rspec_success } - let(:test_case_2_failed) { create_test_case_rspec_failed } - - let(:test_case_1_failed) do - create_test_case_rspec_success.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_FAILED) - end - end + let(:test_case_1_failed) { create_test_case_rspec_failed } + let(:test_case_2_failed) { create_test_case_rspec_failed('case2') } before do base_suite.add_test_case(test_case_1_success) diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb index eacde22cd56..8633a63849f 100644 --- a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -3,6 +3,40 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::TestStage do let(:stage_name) { :test } + let(:project) { create(:project) } + let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } it_behaves_like 'base stage' + + describe '#median' do + before do + issue_1 = create(:issue, project: project, created_at: 90.minutes.ago) + issue_2 = create(:issue, project: project, created_at: 60.minutes.ago) + issue_3 = create(:issue, project: project, created_at: 60.minutes.ago) + mr_1 = create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) + mr_2 = create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') + mr_3 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') + mr_4 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') + mr_5 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D') + mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago) + mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago) + mr_3.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) + mr_4.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) + mr_5.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) + + create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) + create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) + create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3) + create(:merge_requests_closing_issues, merge_request: mr_4, issue: issue_3) + create(:merge_requests_closing_issues, merge_request: mr_5, issue: issue_3) + end + + around do |example| + Timecop.freeze { example.run } + end + + it 'counts median from issues with metrics' do + expect(stage.median).to eq(ISSUES_MEDIAN) + end + end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index c8e65a3e59d..92d90ac2fef 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -87,13 +87,13 @@ describe Gitlab::Danger::Helper do describe '#changes_by_category' do it 'categorizes changed files' do - expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] } + expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } allow(fake_git).to receive(:modified_files) { [] } allow(fake_git).to receive(:renamed_files) { [] } expect(helper.changes_by_category).to eq( backend: %w[foo.rb], - database: %w[db/foo], + database: %w[db/foo lib/gitlab/database/foo.rb], frontend: %w[foo.js], none: %w[ee/changelogs/foo.yml foo.md], qa: %w[qa/foo], @@ -159,9 +159,22 @@ describe Gitlab::Danger::Helper do 'ee/FOO_VERSION' | :unknown - 'db/foo' | :database - 'qa/foo' | :qa + 'db/foo' | :database + 'ee/db/foo' | :database + 'app/models/project_authorization.rb' | :database + 'app/services/users/refresh_authorized_projects_service.rb' | :database + 'lib/gitlab/background_migration.rb' | :database + 'lib/gitlab/background_migration/foo' | :database + 'ee/lib/gitlab/background_migration/foo' | :database + 'lib/gitlab/database.rb' | :database + 'lib/gitlab/database/foo' | :database + 'ee/lib/gitlab/database/foo' | :database + 'lib/gitlab/github_import.rb' | :database + 'lib/gitlab/github_import/foo' | :database + 'lib/gitlab/sql/foo' | :database + 'rubocop/cop/migration/foo' | :database + 'qa/foo' | :qa 'ee/qa/foo' | :qa 'changelogs/foo' | :none diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index aea02d21048..b755cd1aff0 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -610,4 +610,17 @@ describe Gitlab::Diff::Position do it_behaves_like "diff position json" end end + + describe "#file_hash" do + subject do + described_class.new( + old_path: "image.jpg", + new_path: "image.jpg" + ) + end + + it "returns SHA1 representation of the file_path" do + expect(subject.file_hash).to eq(Digest::SHA1.hexdigest(subject.file_path)) + end + end end diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb new file mode 100644 index 00000000000..900816af53a --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer::ImageStrategy do + include PositionTracerHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:file_name) { 'test-file' } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { 'position-tracer-test' } + let(:old_position) { position(old_path: file_name, new_path: file_name, position_type: 'image') } + + let(:tracer) do + Gitlab::Diff::PositionTracer.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) + end + + let(:strategy) { described_class.new(tracer) } + + subject { strategy.trace(old_position) } + + let(:initial_commit) do + project.commit(create_branch(branch_name, 'master')[:branch].name) + end + + describe '#trace' do + describe 'diff scenarios' do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + Base64.encode64('content') + ) + end + + let(:update_file_commit) do + create_file_commit + + update_file( + branch_name, + file_name, + Base64.encode64('updatedcontent') + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + Base64.encode64('updatedcontentagain') + ) + end + + let(:delete_file_commit) do + create_file_commit + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + Base64.encode64('renamedcontent') + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + Base64.encode64('morecontent') + ) + end + + let(:create_another_file_commit) do + create_file( + branch_name, + second_file_name, + Base64.encode64('morecontent') + ) + end + + let(:update_another_file_commit) do + update_file( + branch_name, + second_file_name, + Base64.encode64('updatedmorecontent') + ) + end + + context 'when the file was created in the old diff' do + context 'when the file is unchanged between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + + it 'returns the new position' do + expect_new_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was updated between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was renamed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, rename_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was removed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file is unchanged in the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_another_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + end + + context 'when the file was changed in the old diff' do + context 'when the file is unchanged in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + + it 'returns the new position' do + expect_new_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was updated in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was renamed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, rename_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was removed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, delete_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file is unchanged in the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, create_another_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb new file mode 100644 index 00000000000..7f4902c5b86 --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb @@ -0,0 +1,1805 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer::LineStrategy do + # Douwe's diary New York City, 2016-06-28 + # -------------------------------------------------------------------------- + # + # Dear diary, + # + # Ideally, we would have a test for every single diff scenario that can + # occur and that the PositionTracer should correctly trace a position + # through, across the following variables: + # + # - Old diff file type: created, changed, renamed, deleted, unchanged (5) + # - Old diff line type: added, removed, unchanged (3) + # - New diff file type: created, changed, renamed, deleted, unchanged (5) + # - New diff line type: added, removed, unchanged (3) + # - Old-to-new diff line change: kept, moved, undone (3) + # + # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, + # and 675 different tests to cover them all. In reality, it would be fewer, + # since one cannot have a removed line in a created file diff, for example, + # but for the sake of this diary entry, let's be pessimistic. + # + # Writing these tests is a manual and time consuming process, as every test + # requires the manual construction or finding of a combination of diffs that + # create the exact diff scenario we are looking for, and can take between + # 1 and 10 minutes, depending on the farfetchedness of the scenario and + # complexity of creating it. + # + # This means that writing tests to cover all of these scenarios would end up + # taking between 11 and 112 hours in total, which I do not believe is the + # best use of my time. + # + # A better course of action would be to think of scenarios that are likely + # to occur, but also potentially tricky to trace correctly, and only cover + # those, with a few more obvious scenarios thrown in to cover our bases. + # + # Unfortunately, I only came to the above realization once I was about + # 1/5th of the way through the process of writing ALL THE SPECS, having + # already wasted about 3 hours trying to be thorough. + # + # I did find 2 bugs while writing those though, so that's good. + # + # In any case, all of this means that the tests below will be extremely + # (excessively, unjustifiably) thorough for scenarios where "the file was + # created in the old diff" and then drop off to comparatively lackluster + # testing of other scenarios. + # + # I did still try to cover most of the obvious and potentially tricky + # scenarios, though. + + include RepoHelpers + include PositionTracerHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:repository) { project.repository } + let(:file_name) { "test-file" } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { "position-tracer-test" } + + let(:old_diff_refs) { raise NotImplementedError } + let(:new_diff_refs) { raise NotImplementedError } + let(:change_diff_refs) { raise NotImplementedError } + let(:old_position) { raise NotImplementedError } + + let(:tracer) do + Gitlab::Diff::PositionTracer.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) + end + + let(:strategy) { described_class.new(tracer) } + + subject { strategy.trace(old_position) } + + let(:initial_commit) do + project.commit(create_branch(branch_name, 'master')[:branch].name) + end + + describe "#trace" do + describe "diff scenarios" do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + CONTENT + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + E + CONTENT + ) + end + + let(:update_line_commit) do + create_second_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + CONTENT + ) + end + + let(:update_second_file_line_commit) do + update_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + CONTENT + ) + end + + let(:move_line_commit) do + update_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + C + CONTENT + ) + end + + let(:add_second_file_line_commit) do + move_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + F + CONTENT + ) + end + + let(:move_second_file_line_commit) do + add_second_file_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + EE + CONTENT + ) + end + + let(:delete_line_commit) do + move_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:delete_second_file_line_commit) do + delete_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + CONTENT + ) + end + + let(:delete_file_commit) do + delete_second_file_line_commit + + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:update_line_again_commit) do + rename_file_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + AA + CONTENT + ) + end + + let(:move_line_again_commit) do + update_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + BB + CONTENT + ) + end + + let(:delete_line_again_commit) do + move_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + CONTENT + ) + end + + context "when the file was created in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is renamed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 2 A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 + AA + # 1 2 BB + # 2 - A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: 1, + new_line: 2 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: new_file_name, + old_line: 2, + new_line: nil + ) + end + end + end + end + end + + context "when the file is deleted in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 - A + # 2 - BB + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is unchanged in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 B + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + end + + context "when the file was changed in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when the position pointed at an unchanged line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } + + # old diff: + # 1 1 BB + # 2 2 A + # 3 - C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 1, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 2, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: old_position.old_line, + new_line: nil + ) + end + end + end + end + end + end + + describe "typical use scenarios" do + let(:second_branch_name) { "#{branch_name}-2" } + + def expect_new_positions(old_attrs, new_attrs) + old_positions = old_attrs.map do |old_attrs| + position(old_attrs) + end + + new_positions = old_positions.map do |old_position| + strategy.trace(old_position) + end + + aggregate_failures do + new_positions.zip(new_attrs).each do |new_position, new_attrs| + if new_attrs&.delete(:change) + expect_change_position(new_attrs, new_position) + else + expect_new_position(new_attrs, new_position) + end + end + end + end + + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + D + E + F + CONTENT + ) + end + + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + B + C + D + E + F + CONTENT + ) + end + + let(:update_file_commit) do + second_create_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + describe "simple push of new commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { new_path: file_name, new_line: 4, change: true }, + { new_path: file_name, old_line: 3, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { new_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to overwrite last commit" do + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { new_path: file_name, new_line: 4, change: true }, + { old_path: file_name, old_line: 3, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to delete last commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, old_line: 2, change: true }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, + { old_path: file_name, old_line: 4, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, + { new_path: file_name, new_line: 5, change: true }, + { old_path: file_name, old_line: 6, change: true }, + { new_path: file_name, new_line: 6 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "rebase on top of target branch" do + let(:second_update_file_commit) do + update_file_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + second_update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:overwrite_update_file_again_commit) do + update_file_again_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 } # + G + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "merge of target branch" do + let(:merge_commit) do + second_create_file_commit + + merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) + + repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") + + project.commit(branch_name) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 } # + G + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "changing target branch" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 + BB + # 2 3 C + # 3 - DD + # 4 + D + # 4 5 E + # 5 - F + # 6 + FF + # 7 G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2, change: true }, + { new_path: file_name, new_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, + { new_path: file_name, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, + { old_path: file_name, old_line: 5 }, + { new_path: file_name, new_line: 6 }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 866550753a8..79b33d4d276 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1,1896 +1,98 @@ require 'spec_helper' describe Gitlab::Diff::PositionTracer do - # Douwe's diary New York City, 2016-06-28 - # -------------------------------------------------------------------------- - # - # Dear diary, - # - # Ideally, we would have a test for every single diff scenario that can - # occur and that the PositionTracer should correctly trace a position - # through, across the following variables: - # - # - Old diff file type: created, changed, renamed, deleted, unchanged (5) - # - Old diff line type: added, removed, unchanged (3) - # - New diff file type: created, changed, renamed, deleted, unchanged (5) - # - New diff line type: added, removed, unchanged (3) - # - Old-to-new diff line change: kept, moved, undone (3) - # - # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, - # and 675 different tests to cover them all. In reality, it would be fewer, - # since one cannot have a removed line in a created file diff, for example, - # but for the sake of this diary entry, let's be pessimistic. - # - # Writing these tests is a manual and time consuming process, as every test - # requires the manual construction or finding of a combination of diffs that - # create the exact diff scenario we are looking for, and can take between - # 1 and 10 minutes, depending on the farfetchedness of the scenario and - # complexity of creating it. - # - # This means that writing tests to cover all of these scenarios would end up - # taking between 11 and 112 hours in total, which I do not believe is the - # best use of my time. - # - # A better course of action would be to think of scenarios that are likely - # to occur, but also potentially tricky to trace correctly, and only cover - # those, with a few more obvious scenarios thrown in to cover our bases. - # - # Unfortunately, I only came to the above realization once I was about - # 1/5th of the way through the process of writing ALL THE SPECS, having - # already wasted about 3 hours trying to be thorough. - # - # I did find 2 bugs while writing those though, so that's good. - # - # In any case, all of this means that the tests below will be extremely - # (excessively, unjustifiably) thorough for scenarios where "the file was - # created in the old diff" and then drop off to comparatively lackluster - # testing of other scenarios. - # - # I did still try to cover most of the obvious and potentially tricky - # scenarios, though. + include PositionTracerHelpers - include RepoHelpers - - let(:project) { create(:project, :repository) } - let(:current_user) { project.owner } - let(:repository) { project.repository } - let(:file_name) { "test-file" } - let(:new_file_name) { "#{file_name}-new" } - let(:second_file_name) { "#{file_name}-2" } - let(:branch_name) { "position-tracer-test" } - - let(:old_diff_refs) { raise NotImplementedError } - let(:new_diff_refs) { raise NotImplementedError } - let(:change_diff_refs) { raise NotImplementedError } - let(:old_position) { raise NotImplementedError } - - let(:position_tracer) { described_class.new(project: project, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } - subject { position_tracer.trace(old_position) } - - def diff_refs(base_commit, head_commit) - Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) - end - - def text_position_attrs - [:old_line, :new_line] - end - - def position(attrs = {}) - attrs.reverse_merge!( - diff_refs: old_diff_refs + subject do + described_class.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs ) - Gitlab::Diff::Position.new(attrs) end - def expect_new_position(attrs, result = subject) - aggregate_failures("expect new position #{attrs.inspect}") do - if attrs.nil? - expect(result[:outdated]).to be_truthy - else - expect(result[:outdated]).to be_falsey + describe '#trace' do + let(:diff_refs) { double(complete?: true) } + let(:project) { double } + let(:old_diff_refs) { diff_refs } + let(:new_diff_refs) { diff_refs } + let(:position) { double(on_text?: on_text?, diff_refs: diff_refs) } + let(:tracer) { double } - new_position = result[:position] - expect(new_position).not_to be_nil + context 'position is on text' do + let(:on_text?) { true } - expect(new_position.diff_refs).to eq(new_diff_refs) + it 'calls LineStrategy#trace' do + expect(Gitlab::Diff::PositionTracer::LineStrategy) + .to receive(:new) + .with(subject) + .and_return(tracer) + expect(tracer).to receive(:trace).with(position) - attrs.each do |attr, value| - if text_position_attrs.include?(attr) - expect(new_position.formatter.send(attr)).to eq(value) - else - expect(new_position.send(attr)).to eq(value) - end - end + subject.trace(position) end end - end - - def expect_change_position(attrs, result = subject) - aggregate_failures("expect change position #{attrs.inspect}") do - expect(result[:outdated]).to be_truthy - - change_position = result[:position] - if attrs.nil? || attrs.empty? - expect(change_position).to be_nil - else - expect(change_position).not_to be_nil - - expect(change_position.diff_refs).to eq(change_diff_refs) - - attrs.each do |attr, value| - if text_position_attrs.include?(attr) - expect(change_position.formatter.send(attr)).to eq(value) - else - expect(change_position.send(attr)).to eq(value) - end - end - end - end - end - - def create_branch(new_name, branch_name) - CreateBranchService.new(project, current_user).execute(new_name, branch_name) - end - - def create_file(branch_name, file_name, content) - Files::CreateService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Create file", - file_path: file_name, - file_content: content - ).execute - project.commit(branch_name) - end - - def update_file(branch_name, file_name, content) - Files::UpdateService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Update file", - file_path: file_name, - file_content: content - ).execute - project.commit(branch_name) - end - - def delete_file(branch_name, file_name) - Files::DeleteService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Delete file", - file_path: file_name - ).execute - project.commit(branch_name) - end - - let(:initial_commit) do - create_branch(branch_name, "master")[:branch].name - project.commit(branch_name) - end - - describe "#trace" do - describe "diff scenarios" do - let(:create_file_commit) do - initial_commit - - create_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - B - C - CONTENT - ) - end - - let(:create_second_file_commit) do - create_file_commit - - create_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - E - CONTENT - ) - end - - let(:update_line_commit) do - create_second_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - CONTENT - ) - end - - let(:update_second_file_line_commit) do - update_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - EE - CONTENT - ) - end - - let(:move_line_commit) do - update_second_file_line_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - BB - A - C - CONTENT - ) - end - - let(:add_second_file_line_commit) do - move_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - EE - F - CONTENT - ) - end - - let(:move_second_file_line_commit) do - add_second_file_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - F - EE - CONTENT - ) - end - - let(:delete_line_commit) do - move_second_file_line_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - BB - A - CONTENT - ) - end - - let(:delete_second_file_line_commit) do - delete_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - F - CONTENT - ) - end - - let(:delete_file_commit) do - delete_second_file_line_commit - - delete_file(branch_name, file_name) - end - - let(:rename_file_commit) do - delete_file_commit - - create_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - BB - A - CONTENT - ) - end - - let(:update_line_again_commit) do - rename_file_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - BB - AA - CONTENT - ) - end - - let(:move_line_again_commit) do - update_line_again_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - AA - BB - CONTENT - ) - end - - let(:delete_line_again_commit) do - move_line_again_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - AA - CONTENT - ) - end - - context "when the file was created in the old diff" do - context "when the file is created in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + B - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 + BB - # 2 + A - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is changed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is renamed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 2 A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 2 - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 - A - # 2 + AA - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: old_position.new_line, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 + AA - # 1 2 BB - # 2 - A - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: 1, - new_line: 2 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 - A - # 2 + AA - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: new_file_name, - old_line: 2, - new_line: nil - ) - end - end - end - end - end - - context "when the file is deleted in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 - A - # 2 - BB - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A + context 'position is not on text' do + let(:on_text?) { false } - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end + it 'calls ImageStrategy#trace' do + expect(Gitlab::Diff::PositionTracer::ImageStrategy) + .to receive(:new) + .with(subject) + .and_return(tracer) + expect(tracer).to receive(:trace).with(position) - context "when the file is unchanged in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 B - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 2 - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - end - - context "when the file was changed in the old diff" do - context "when the file is created in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + A - # 2 + B - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 1, - new_line: nil - ) - end - end - end - end - - context "when the position pointed at a deleted line in the old diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the position pointed at an unchanged line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } - - # old diff: - # 1 1 BB - # 2 2 A - # 3 - C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + A - # 2 + B - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is changed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 - C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: 1, - new_line: 1 - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: 2, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 1, - new_line: nil - ) - end - end - end - end - - context "when the position pointed at a deleted line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: old_position.old_line, - new_line: nil - ) - end - end - end - end + subject.trace(position) end end end - describe "typical use scenarios" do - let(:second_branch_name) { "#{branch_name}-2" } - - def expect_new_positions(old_attrs, new_attrs) - old_positions = old_attrs.map do |old_attrs| - position(old_attrs) - end + describe 'diffs methods' do + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } - new_positions = old_positions.map do |old_position| - position_tracer.trace(old_position) - end - - aggregate_failures do - new_positions.zip(new_attrs).each do |new_position, new_attrs| - if new_attrs&.delete(:change) - expect_change_position(new_attrs, new_position) - else - expect_new_position(new_attrs, new_position) - end - end - end - end - - let(:create_file_commit) do - initial_commit - - create_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - B - C - D - E - F - CONTENT - ) - end - - let(:second_create_file_commit) do - create_file_commit - - create_branch(second_branch_name, branch_name) - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - B - C - D - E - F - CONTENT + let(:old_diff_refs) do + diff_refs( + project.commit(create_branch('new-branch', 'master')[:branch].name), + create_file('new-branch', 'file.md', 'content') ) end - let(:update_file_commit) do - second_create_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - C - DD - E - F - G - CONTENT - ) - end - - let(:update_file_again_commit) do - update_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT + let(:new_diff_refs) do + diff_refs( + create_file('new-branch', 'file.md', 'content'), + update_file('new-branch', 'file.md', 'updatedcontent') ) end - describe "simple push of new commit" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C - { old_path: file_name, old_line: 4 }, # - D - { new_path: file_name, new_line: 3 }, # + DD - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F - { new_path: file_name, new_line: 6 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { new_path: file_name, new_line: 4, change: true }, - { new_path: file_name, old_line: 3, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { new_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } - ] + describe '#ac_diffs' do + it 'returns the diffs between the base of old and new diff' do + diff_refs = subject.ac_diffs.diff_refs - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(old_diff_refs.base_sha) + expect(diff_refs.start_sha).to eq(old_diff_refs.base_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.base_sha) end end - describe "force push to overwrite last commit" do - let(:second_create_file_commit) do - create_file_commit + describe '#bd_diffs' do + it 'returns the diffs between the HEAD of old and new diff' do + diff_refs = subject.bd_diffs.diff_refs - create_branch(second_branch_name, branch_name) - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } - let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C - { old_path: file_name, old_line: 4 }, # - D - { new_path: file_name, new_line: 3 }, # + DD - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F - { new_path: file_name, new_line: 6 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { new_path: file_name, new_line: 4, change: true }, - { old_path: file_name, old_line: 3, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { old_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(old_diff_refs.head_sha) + expect(diff_refs.start_sha).to eq(old_diff_refs.head_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end - describe "force push to delete last commit" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, old_line: 2, change: true }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, - { old_path: file_name, old_line: 4, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, - { new_path: file_name, new_line: 5, change: true }, - { old_path: file_name, old_line: 6, change: true }, - { new_path: file_name, new_line: 6 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "rebase on top of target branch" do - let(:second_update_file_commit) do - update_file_commit - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - C - DD - E - F - G - CONTENT - ) - end - - let(:update_file_again_commit) do - second_update_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:overwrite_update_file_again_commit) do - update_file_again_commit - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 + Z - # 2 + Z - # 3 + Z - # 1 4 A - # 2 - B - # 5 + BB - # 3 6 C - # 4 7 D - # 5 8 E - # 6 - F - # 9 + FF - # 0 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 5 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 9 }, # + FF - { new_path: file_name, new_line: 10 }, # + G - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "merge of target branch" do - let(:merge_commit) do - second_create_file_commit - - merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) - - repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") - - project.commit(branch_name) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 + Z - # 2 + Z - # 3 + Z - # 1 4 A - # 2 - B - # 5 + BB - # 3 6 C - # 4 7 D - # 5 8 E - # 6 - F - # 9 + FF - # 0 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 5 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 9 }, # + FF - { new_path: file_name, new_line: 10 }, # + G - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "changing target branch" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 1 A - # 2 + BB - # 2 3 C - # 3 - DD - # 4 + D - # 4 5 E - # 5 - F - # 6 + FF - # 7 G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2, change: true }, - { new_path: file_name, new_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, - { new_path: file_name, new_line: 4 }, - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, - { old_path: file_name, old_line: 5 }, - { new_path: file_name, new_line: 6 }, - { new_path: file_name, new_line: 7 } - ] + describe '#cd_diffs' do + it 'returns the diffs in the new diff' do + diff_refs = subject.cd_diffs.diff_refs - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(new_diff_refs.base_sha) + expect(diff_refs.start_sha).to eq(new_diff_refs.base_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 25052a79916..3f0e6b34291 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -408,7 +408,7 @@ describe Gitlab::Git::Commit, :seed_helper do context 'when oids is empty' do it 'makes no Gitaly request' do - expect(Gitlab::GitalyClient).not_to receive(:call) + expect(Gitlab::GitalyClient).not_to receive(:call).with(repository.storage, :commit_service, :list_commits_by_oid) described_class.batch_by_oid(repository, []) end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cceeae8afe6..a28b95e5bff 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1694,14 +1694,15 @@ describe Gitlab::Git::Repository, :seed_helper do let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:right_branch) { 'test-master' } + let(:first_parent_ref) { 'refs/heads/test-master' } let(:target_ref) { 'refs/merge-requests/999/merge' } before do - repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref) end def merge_to_ref - repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref) end it 'generates a commit in the target_ref' do @@ -1716,7 +1717,7 @@ describe Gitlab::Git::Repository, :seed_helper do end it 'does not change the right branch HEAD' do - expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + expect { merge_to_ref }.not_to change { repository.commit(first_parent_ref).sha } end end diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb new file mode 100644 index 00000000000..f957ed00945 --- /dev/null +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'json' +require 'tempfile' + +describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:feature_flag_name) { 'feature-flag-name' } + let(:feature_flag) { Feature.get(feature_flag_name) } + let(:temp_gitaly_metadata_file) { create_temporary_gitaly_metadata_file } + + before(:all) do + create_gitaly_metadata_file + end + + subject(:wrapper) do + klazz = Class.new { include Gitlab::Git::RuggedImpl::UseRugged } + klazz.new + end + + before do + Gitlab::GitalyClient.instance_variable_set(:@can_use_disk, {}) + end + + context 'when feature flag is not persisted' do + before do + allow(Feature).to receive(:persisted?).with(feature_flag).and_return(false) + end + + it 'returns true when gitaly matches disk' do + expect(subject.use_rugged?(repository, feature_flag_name)).to be true + end + + it 'returns false when disk access fails' do + allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return("/fake/path/doesnt/exist") + + expect(subject.use_rugged?(repository, feature_flag_name)).to be false + end + + it "returns false when gitaly doesn't match disk" do + allow(Gitlab::GitalyClient).to receive(:storage_metadata_file_path).and_return(temp_gitaly_metadata_file) + + expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey + + File.delete(temp_gitaly_metadata_file) + end + + it "doesn't lead to a second rpc call because gitaly client should use the cached value" do + expect(subject.use_rugged?(repository, feature_flag_name)).to be true + + expect(Gitlab::GitalyClient).not_to receive(:filesystem_id) + + subject.use_rugged?(repository, feature_flag_name) + end + end + + context 'when feature flag is persisted' do + before do + allow(Feature).to receive(:persisted?).with(feature_flag).and_return(true) + end + + it 'returns false when the feature flag is off' do + allow(feature_flag).to receive(:enabled?).and_return(false) + + expect(subject.use_rugged?(repository, feature_flag_name)).to be_falsey + end + + it "returns true when feature flag is on" do + allow(feature_flag).to receive(:enabled?).and_return(true) + allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(false) + + expect(subject.use_rugged?(repository, feature_flag_name)).to be true + end + end + + def create_temporary_gitaly_metadata_file + tmp = Tempfile.new('.gitaly-metadata') + gitaly_metadata = { + "gitaly_filesystem_id" => "some-value" + } + tmp.write(gitaly_metadata.to_json) + tmp.flush + tmp.close + tmp.path + end + + def create_gitaly_metadata_file + File.open(File.join(SEED_STORAGE_PATH, '.gitaly-metadata'), 'w+') do |f| + gitaly_metadata = { + "gitaly_filesystem_id" => SecureRandom.uuid + } + f.write(gitaly_metadata.to_json) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 18663a72fcd..f38b8d31237 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -79,13 +79,13 @@ describe Gitlab::GitalyClient::OperationService do end describe '#user_merge_to_ref' do - let(:branch) { 'my-branch' } + let(:first_parent_ref) { 'refs/heads/my-branch' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:ref) { 'refs/merge-requests/x/merge' } let(:message) { 'validación' } let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } - subject { client.user_merge_to_ref(user, source_sha, branch, ref, message) } + subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref) } it 'sends a user_merge_to_ref message' do expect_any_instance_of(Gitaly::OperationService::Stub) diff --git a/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb new file mode 100644 index 00000000000..d93ce464a92 --- /dev/null +++ b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Graphql::CallsGitaly::Instrumentation do + subject { described_class.new } + + describe '#calls_gitaly_check' do + let(:gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) } + let(:no_gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) } + + context 'if there are no Gitaly calls' do + it 'does not raise an error if calls_gitaly is false' do + expect { subject.send(:calls_gitaly_check, no_gitaly_field, 0) }.not_to raise_error + end + end + + context 'if there is at least 1 Gitaly call' do + it 'raises an error if calls_gitaly: is false or not defined' do + expect { subject.send(:calls_gitaly_check, no_gitaly_field, 1) }.to raise_error(/specify a constant complexity or add `calls_gitaly: true`/) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index aaf8c9fa2a0..4d93b70e6e3 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -8,12 +8,19 @@ describe Gitlab::Metrics::Samplers::RubySampler do allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric) end + describe '#initialize' do + it 'sets process_start_time_seconds' do + Timecop.freeze do + expect(sampler.metrics[:process_start_time_seconds].get).to eq(Time.now.to_i) + end + end + end + describe '#sample' do it 'samples various statistics' do expect(Gitlab::Metrics::System).to receive(:cpu_time) expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) expect(Gitlab::Metrics::System).to receive(:memory_usage) - expect(Gitlab::Metrics::System).to receive(:process_start_time) expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors) expect(sampler).to receive(:sample_gc) @@ -44,13 +51,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do sampler.sample end - it 'adds a metric containing the process start time' do - expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345) - expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345) - - sampler.sample - end - it 'adds a metric containing the process max file descriptors' do expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024) expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024) diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index da87df15746..3b434a02f63 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -19,12 +19,6 @@ describe Gitlab::Metrics::System do expect(described_class.max_open_file_descriptors).to be > 0 end end - - describe '.process_start_time' do - it 'returns the process start time' do - expect(described_class.process_start_time).to be > 0 - end - end else describe '.memory_usage' do it 'returns 0.0' do @@ -43,12 +37,6 @@ describe Gitlab::Metrics::System do expect(described_class.max_open_file_descriptors).to eq(0) end end - - describe 'process_start_time' do - it 'returns 0' do - expect(described_class.process_start_time).to eq(0) - end - end end describe '.cpu_time' do diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb index c0af2ede32a..e177c44ad67 100644 --- a/spec/lib/gitlab/namespaced_session_store_spec.rb +++ b/spec/lib/gitlab/namespaced_session_store_spec.rb @@ -4,19 +4,33 @@ require 'spec_helper' describe Gitlab::NamespacedSessionStore do let(:key) { :some_key } - subject { described_class.new(key) } - it 'stores data under the specified key' do - Gitlab::Session.with_session({}) do - subject[:new_data] = 123 + context 'current session' do + subject { described_class.new(key) } - expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + it 'stores data under the specified key' do + Gitlab::Session.with_session({}) do + subject[:new_data] = 123 + + expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + end + end + + it 'retrieves data from the given key' do + Thread.current[:session_storage] = { key => { existing_data: 123 } } + + expect(subject[:existing_data]).to eq 123 end end - it 'retrieves data from the given key' do - Thread.current[:session_storage] = { key => { existing_data: 123 } } + context 'passed in session' do + let(:data) { { 'data' => 42 } } + let(:session) { { 'some_key' => data } } + + subject { described_class.new(key, session.with_indifferent_access) } - expect(subject[:existing_data]).to eq 123 + it 'retrieves data from the given key' do + expect(subject['data']).to eq 42 + end end end diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb index f480376acb4..ee3c571c9c0 100644 --- a/spec/lib/gitlab/performance_bar_spec.rb +++ b/spec/lib/gitlab/performance_bar_spec.rb @@ -3,17 +3,42 @@ require 'spec_helper' describe Gitlab::PerformanceBar do shared_examples 'allowed user IDs are cached' do before do - # Warm the Redis cache + # Warm the caches described_class.enabled?(user) end it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do expect do + expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original + expect(described_class.l2_cache_backend).not_to receive(:fetch) expect(described_class.enabled?(user)).to be_truthy end.not_to exceed_query_limit(0) end + + it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do + Timecop.travel 2.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original + expect(described_class.enabled?(user)).to be_truthy + end.not_to exceed_query_limit(0) + end + end + + it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do + Timecop.travel 6.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original + expect(described_class.enabled?(user)).to be_truthy + end.not_to exceed_query_limit(2) + end + end end + it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) } + it { expect(described_class.l2_cache_backend).to eq(Rails.cache) } + describe '.enabled?' do let(:user) { create(:user) } diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 5b5052de372..98838712eae 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -10,6 +10,12 @@ describe Gitlab::SQL::Pattern do it 'returns exact matching pattern' do expect(to_pattern).to eq('12') end + + context 'and ignore_minimum_char_limit is true' do + it 'returns partial matching pattern' do + expect(User.to_pattern(query, use_minimum_char_limit: false)).to eq('%12%') + end + end end context 'when a query with a escape character is shorter than 3 chars' do @@ -18,6 +24,12 @@ describe Gitlab::SQL::Pattern do it 'returns sanitized exact matching pattern' do expect(to_pattern).to eq('\_2') end + + context 'and ignore_minimum_char_limit is true' do + it 'returns sanitized partial matching pattern' do + expect(User.to_pattern(query, use_minimum_char_limit: false)).to eq('%\_2%') + end + end end context 'when a query is equal to 3 chars' do diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb deleted file mode 100644 index b86ec5445b8..00000000000 --- a/spec/lib/gitlab/user_extractor_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe Gitlab::UserExtractor do - let(:text) do - <<~TXT - This is a long texth that mentions some users. - @user-1, @user-2 and user@gitlab.org take a walk in the park. - There they meet @user-4 that was out with other-user@gitlab.org. - @user-1 thought it was late, so went home straight away - TXT - end - subject(:extractor) { described_class.new(text) } - - describe '#users' do - it 'returns an empty relation when nil was passed' do - extractor = described_class.new(nil) - - expect(extractor.users).to be_empty - expect(extractor.users).to be_a(ActiveRecord::Relation) - end - - it 'returns the user case insensitive for usernames' do - user = create(:user, username: "USER-4") - - expect(extractor.users).to include(user) - end - - it 'returns users by primary email' do - user = create(:user, email: 'user@gitlab.org') - - expect(extractor.users).to include(user) - end - - it 'returns users by secondary email' do - user = create(:email, email: 'other-user@gitlab.org').user - - expect(extractor.users).to include(user) - end - - context 'input as array of strings' do - it 'is treated as one string' do - extractor = described_class.new(text.lines) - - user_1 = create(:user, username: "USER-1") - user_4 = create(:user, username: "USER-4") - user_email = create(:user, email: 'user@gitlab.org') - - expect(extractor.users).to contain_exactly(user_1, user_4, user_email) - end - end - end - - describe '#matches' do - it 'includes all mentioned email adresses' do - expect(extractor.matches[:emails]).to contain_exactly('user@gitlab.org', 'other-user@gitlab.org') - end - - it 'includes all mentioned usernames' do - expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4') - end - - context 'input has no matching e-mail or usernames' do - it 'returns an empty list of users' do - extractor = described_class.new('My test') - - expect(extractor.users).to be_empty - end - end - end - - describe '#references' do - it 'includes all user-references once' do - expect(extractor.references).to contain_exactly('user-1', 'user-2', 'user@gitlab.org', 'user-4', 'other-user@gitlab.org') - end - end -end diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb new file mode 100644 index 00000000000..da13b6df53b --- /dev/null +++ b/spec/lib/peek/views/redis_detailed_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Peek::Views::RedisDetailed do + let(:redis_detailed_class) do + Class.new do + include Peek::Views::RedisDetailed + end + end + + subject { redis_detailed_class.new } + + using RSpec::Parameterized::TableSyntax + + where(:cmd, :expected) do + [:auth, 'test'] | 'auth <redacted>' + [:set, 'key', 'value'] | 'set key <redacted>' + [:set, 'bad'] | 'set bad' + [:hmset, 'key1', 'value1', 'key2', 'value2'] | 'hmset key1 <redacted>' + [:get, 'key'] | 'get key' + end + + with_them do + it 'scrubs Redis commands', :request_store do + subject.detail_store << { cmd: cmd, duration: 1.second } + + expect(subject.details.count).to eq(1) + expect(subject.details.first) + .to eq({ + cmd: expected, + duration: 1000 + }) + end + end +end diff --git a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb index 34f4a36d63d..65a918d5440 100644 --- a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb +++ b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb @@ -13,7 +13,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do subject(:migration) { described_class.new } around do |example| - Sidekiq::Testing.inline! do + perform_enqueued_jobs do example.run end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 55cea48b641..e24bbc39761 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2998,4 +2998,28 @@ describe Ci::Pipeline, :mailer do end end end + + describe '#error_messages' do + subject { pipeline.error_messages } + + before do + pipeline.valid? + end + + context 'when pipeline has errors' do + let(:pipeline) { build(:ci_pipeline, sha: nil, ref: nil) } + + it 'returns the full error messages' do + is_expected.to eq("Sha can't be blank and Ref can't be blank") + end + end + + context 'when pipeline does not have errors' do + let(:pipeline) { build(:ci_pipeline) } + + it 'returns empty string' do + is_expected.to be_empty + end + end + end end diff --git a/spec/models/clusters/clusters_hierarchy_spec.rb b/spec/models/clusters/clusters_hierarchy_spec.rb new file mode 100644 index 00000000000..0470ebe17ea --- /dev/null +++ b/spec/models/clusters/clusters_hierarchy_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::ClustersHierarchy do + describe '#base_and_ancestors' do + def base_and_ancestors(clusterable) + described_class.new(clusterable).base_and_ancestors + end + + context 'project in nested group with clusters at every level' do + let!(:cluster) { create(:cluster, :project, projects: [project]) } + let!(:child) { create(:cluster, :group, groups: [child_group]) } + let!(:parent) { create(:cluster, :group, groups: [parent_group]) } + let!(:ancestor) { create(:cluster, :group, groups: [ancestor_group]) } + + let(:ancestor_group) { create(:group) } + let(:parent_group) { create(:group, parent: ancestor_group) } + let(:child_group) { create(:group, parent: parent_group) } + let(:project) { create(:project, group: child_group) } + + it 'returns clusters for project' do + expect(base_and_ancestors(project)).to eq([cluster, child, parent, ancestor]) + end + + it 'returns clusters for child_group' do + expect(base_and_ancestors(child_group)).to eq([child, parent, ancestor]) + end + + it 'returns clusters for parent_group' do + expect(base_and_ancestors(parent_group)).to eq([parent, ancestor]) + end + + it 'returns clusters for ancestor_group' do + expect(base_and_ancestors(ancestor_group)).to eq([ancestor]) + end + end + + context 'project in a namespace' do + let!(:cluster) { create(:cluster, :project) } + + it 'returns clusters for project' do + expect(base_and_ancestors(cluster.project)).to eq([cluster]) + end + end + + context 'project in nested group with clusters at some levels' do + let!(:child) { create(:cluster, :group, groups: [child_group]) } + let!(:ancestor) { create(:cluster, :group, groups: [ancestor_group]) } + + let(:ancestor_group) { create(:group) } + let(:parent_group) { create(:group, parent: ancestor_group) } + let(:child_group) { create(:group, parent: parent_group) } + let(:project) { create(:project, group: child_group) } + + it 'returns clusters for project' do + expect(base_and_ancestors(project)).to eq([child, ancestor]) + end + + it 'returns clusters for child_group' do + expect(base_and_ancestors(child_group)).to eq([child, ancestor]) + end + + it 'returns clusters for parent_group' do + expect(base_and_ancestors(parent_group)).to eq([ancestor]) + end + + it 'returns clusters for ancestor_group' do + expect(base_and_ancestors(ancestor_group)).to eq([ancestor]) + end + end + end +end diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb index 2378f400540..e2fc8a5d127 100644 --- a/spec/models/concerns/deployment_platform_spec.rb +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe DeploymentPlatform do let(:project) { create(:project) } - describe '#deployment_platform' do + shared_examples '#deployment_platform' do subject { project.deployment_platform } context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service' do @@ -84,4 +84,20 @@ describe DeploymentPlatform do end end end + + context 'legacy implementation' do + before do + stub_feature_flags(clusters_cte: false) + end + + include_examples '#deployment_platform' + end + + context 'CTE implementation' do + before do + stub_feature_flags(clusters_cte: true) + end + + include_examples '#deployment_platform' + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 64f02978d79..68224a56515 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -223,6 +223,16 @@ describe Issuable do expect(issuable_class.full_search(searchable_issue2.description.downcase)).to eq([searchable_issue2]) end + it 'returns issues with a fuzzy matching description for a query shorter than 3 chars if told to do so' do + search = searchable_issue2.description.downcase.scan(/\w+/).sample[-1] + + expect(issuable_class.full_search(search, use_minimum_char_limit: false)).to include(searchable_issue2) + end + + it 'returns issues with a fuzzy matching title for a query shorter than 3 chars if told to do so' do + expect(issuable_class.full_search('i', use_minimum_char_limit: false)).to include(searchable_issue) + end + context 'when matching columns is "title"' do it 'returns issues with a matching title' do expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title')) diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 7faa196623f..3d026932f59 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -47,30 +47,12 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do subject(:go!) { instance.result } - context 'when cache is empty' do - it { is_expected.to be_nil } - - it 'enqueues a background worker to bootstrap the cache' do - expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) - - go! - end - - it 'updates the cache lifespan' do - expect(reactive_cache_alive?(instance)).to be_falsy - - go! - - expect(reactive_cache_alive?(instance)).to be_truthy - end - end - - context 'when the cache is full' do + shared_examples 'a cacheable value' do |cached_value| before do - stub_reactive_cache(instance, 4) + stub_reactive_cache(instance, cached_value) end - it { is_expected.to eq(4) } + it { is_expected.to eq(cached_value) } it 'does not enqueue a background worker' do expect(ReactiveCachingWorker).not_to receive(:perform_async) @@ -90,9 +72,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end it { is_expected.to be_nil } - end - context 'when cache was invalidated' do it 'refreshes cache' do expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) @@ -101,12 +81,34 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end end - context 'when cache contains non-nil but blank value' do - before do - stub_reactive_cache(instance, false) + context 'when cache is empty' do + it { is_expected.to be_nil } + + it 'enqueues a background worker to bootstrap the cache' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666) + + go! end - it { is_expected.to eq(false) } + it 'updates the cache lifespan' do + expect(reactive_cache_alive?(instance)).to be_falsy + + go! + + expect(reactive_cache_alive?(instance)).to be_truthy + end + end + + context 'when the cache is full' do + it_behaves_like 'a cacheable value', 4 + end + + context 'when the cache contains non-nil but blank value' do + it_behaves_like 'a cacheable value', false + end + + context 'when the cache contains nil value' do + it_behaves_like 'a cacheable value', nil end end @@ -206,8 +208,9 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do expect(read_reactive_cache(instance)).to eq("preexisting") end - it 'enqueues a repeat worker' do - expect_reactive_cache_update_queued(instance) + it 'does not enqueue a repeat worker' do + expect(ReactiveCachingWorker) + .not_to receive(:perform_in) expect { go! }.to raise_error("foo") end diff --git a/spec/models/deployment_metrics_spec.rb b/spec/models/deployment_metrics_spec.rb new file mode 100644 index 00000000000..0aadb1f3a5e --- /dev/null +++ b/spec/models/deployment_metrics_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DeploymentMetrics do + describe '#has_metrics?' do + subject { described_class.new(deployment.project, deployment).has_metrics? } + + context 'when deployment is failed' do + let(:deployment) { create(:deployment, :failed) } + + it { is_expected.to be_falsy } + end + + context 'when deployment is success' do + let(:deployment) { create(:deployment, :success) } + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + + context 'with a Prometheus Service' do + let(:prometheus_service) { instance_double(PrometheusService, can_query?: true) } + + before do + allow(deployment.project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service + end + + it { is_expected.to be_truthy } + end + + context 'with a Prometheus Service that cannot query' do + let(:prometheus_service) { instance_double(PrometheusService, can_query?: false) } + + before do + allow(deployment.project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service + end + + it { is_expected.to be_falsy } + end + + context 'with a cluster Prometheus' do + let(:deployment) { create(:deployment, :success, :on_cluster) } + let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: deployment.cluster) } + + before do + expect(deployment.cluster.application_prometheus).to receive(:can_query?).and_return(true) + end + + it { is_expected.to be_truthy } + end + + context 'fallback deployment platform' do + let(:cluster) { create(:cluster, :provided_by_user, environment_scope: '*', projects: [deployment.project]) } + let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + + before do + expect(deployment.project).to receive(:deployment_platform).and_return(cluster.platform) + expect(cluster.application_prometheus).to receive(:can_query?).and_return(true) + end + + it { is_expected.to be_truthy } + end + end + end + + describe '#metrics' do + let(:deployment) { create(:deployment, :success) } + let(:prometheus_adapter) { instance_double(PrometheusService, can_query?: true) } + let(:deployment_metrics) { described_class.new(deployment.project, deployment) } + + subject { deployment_metrics.metrics } + + context 'metrics are disabled' do + it { is_expected.to eq({}) } + end + + context 'metrics are enabled' do + let(:simple_metrics) do + { + success: true, + metrics: {}, + last_update: 42 + } + end + + before do + allow(deployment_metrics).to receive(:prometheus_adapter).and_return(prometheus_adapter) + expect(prometheus_adapter).to receive(:query).with(:deployment, deployment).and_return(simple_metrics) + end + + it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) } + end + end + + describe '#additional_metrics' do + let(:project) { create(:project, :repository) } + let(:deployment) { create(:deployment, :succeed, project: project) } + let(:deployment_metrics) { described_class.new(deployment.project, deployment) } + + subject { deployment_metrics.additional_metrics } + + context 'metrics are disabled' do + it { is_expected.to eq({}) } + end + + context 'metrics are enabled' do + let(:simple_metrics) do + { + success: true, + metrics: {}, + last_update: 42 + } + end + + let(:prometheus_adapter) { instance_double('prometheus_adapter', can_query?: true) } + + before do + allow(deployment_metrics).to receive(:prometheus_adapter).and_return(prometheus_adapter) + expect(prometheus_adapter).to receive(:query).with(:additional_metrics_deployment, deployment).and_return(simple_metrics) + end + + it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) } + end + end +end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 8d0eb0f4a06..d4e631f109b 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -295,125 +295,6 @@ describe Deployment do end end - describe '#has_metrics?' do - subject { deployment.has_metrics? } - - context 'when deployment is failed' do - let(:deployment) { create(:deployment, :failed) } - - it { is_expected.to be_falsy } - end - - context 'when deployment is success' do - let(:deployment) { create(:deployment, :success) } - - context 'without a monitoring service' do - it { is_expected.to be_falsy } - end - - context 'with a Prometheus Service' do - let(:prometheus_service) { double(:prometheus_service, can_query?: true) } - - before do - allow(deployment.project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service - end - - it { is_expected.to be_truthy } - end - - context 'with a Prometheus Service that cannot query' do - let(:prometheus_service) { double(:prometheus_service, can_query?: false) } - - before do - allow(deployment.project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service - end - - it { is_expected.to be_falsy } - end - - context 'with a cluster Prometheus' do - let(:deployment) { create(:deployment, :success, :on_cluster) } - let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: deployment.cluster) } - - before do - expect(deployment.cluster.application_prometheus).to receive(:can_query?).and_return(true) - end - - it { is_expected.to be_truthy } - end - - context 'fallback deployment platform' do - let(:cluster) { create(:cluster, :provided_by_user, environment_scope: '*', projects: [deployment.project]) } - let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } - - before do - expect(deployment.project).to receive(:deployment_platform).and_return(cluster.platform) - expect(cluster.application_prometheus).to receive(:can_query?).and_return(true) - end - - it { is_expected.to be_truthy } - end - end - end - - describe '#metrics' do - let(:deployment) { create(:deployment, :success) } - let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } - - subject { deployment.metrics } - - context 'metrics are disabled' do - it { is_expected.to eq({}) } - end - - context 'metrics are enabled' do - let(:simple_metrics) do - { - success: true, - metrics: {}, - last_update: 42 - } - end - - before do - allow(deployment).to receive(:prometheus_adapter).and_return(prometheus_adapter) - allow(prometheus_adapter).to receive(:query).with(:deployment, deployment).and_return(simple_metrics) - end - - it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) } - end - end - - describe '#additional_metrics' do - let(:project) { create(:project, :repository) } - let(:deployment) { create(:deployment, :succeed, project: project) } - - subject { deployment.additional_metrics } - - context 'metrics are disabled' do - it { is_expected.to eq({}) } - end - - context 'metrics are enabled' do - let(:simple_metrics) do - { - success: true, - metrics: {}, - last_update: 42 - } - end - - let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } - - before do - allow(deployment).to receive(:prometheus_adapter).and_return(prometheus_adapter) - allow(prometheus_adapter).to receive(:query).with(:additional_metrics_deployment, deployment).and_return(simple_metrics) - end - - it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) } - end - end - describe '#stop_action' do let(:build) { create(:ci_build) } @@ -441,38 +322,4 @@ describe Deployment do end end end - - describe '#deployment_platform_cluster' do - let(:deployment) { create(:deployment) } - let(:project) { deployment.project } - let(:environment) { deployment.environment } - - subject { deployment.deployment_platform_cluster } - - before do - expect(project).to receive(:deployment_platform) - .with(environment: environment.name).and_call_original - end - - context 'project has no deployment platform' do - before do - expect(project.clusters).to be_empty - end - - it { is_expected.to be_nil } - end - - context 'project uses the kubernetes service for deployments' do - let!(:service) { create(:kubernetes_service, project: project) } - - it { is_expected.to be_nil } - end - - context 'project has a deployment platform' do - let!(:cluster) { create(:cluster, projects: [project]) } - let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) } - - it { is_expected.to eq cluster } - end - end end diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index c503c35305f..e2836420df9 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -11,11 +11,10 @@ describe EnvironmentStatus do let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } let(:sha) { deployment.sha } - subject(:environment_status) { described_class.new(environment, merge_request, sha) } + subject(:environment_status) { described_class.new(project, environment, merge_request, sha) } it { is_expected.to delegate_method(:id).to(:environment) } it { is_expected.to delegate_method(:name).to(:environment) } - it { is_expected.to delegate_method(:project).to(:environment) } it { is_expected.to delegate_method(:deployed_at).to(:deployment) } it { is_expected.to delegate_method(:status).to(:deployment) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a2547755510..9b0c232f370 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -7,6 +7,8 @@ describe MergeRequest do include ProjectForksHelper include ReactiveCachingHelpers + using RSpec::Parameterized::TableSyntax + subject { create(:merge_request) } describe 'associations' do @@ -1996,6 +1998,47 @@ describe MergeRequest do end end + describe '#rebase_async' do + let(:merge_request) { create(:merge_request) } + let(:user_id) { double(:user_id) } + let(:rebase_jid) { 'rebase-jid' } + + subject(:execute) { merge_request.rebase_async(user_id) } + + it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do + expect(RebaseWorker) + .to receive(:perform_async) + .with(merge_request.id, user_id) + .and_return(rebase_jid) + + expect(merge_request).to receive(:lock!).and_call_original + + execute + + expect(merge_request.rebase_jid).to eq(rebase_jid) + end + + it 'refuses to enqueue a job if a rebase is in progress' do + merge_request.update_column(:rebase_jid, rebase_jid) + + expect(RebaseWorker).not_to receive(:perform_async) + expect(Gitlab::SidekiqStatus) + .to receive(:running?) + .with(rebase_jid) + .and_return(true) + + expect { execute }.to raise_error(ActiveRecord::StaleObjectError) + end + + it 'refuses to enqueue a job if the MR is not open' do + merge_request.update_column(:state, 'foo') + + expect(RebaseWorker).not_to receive(:perform_async) + + expect { execute }.to raise_error(ActiveRecord::StaleObjectError) + end + end + describe '#mergeable?' do let(:project) { create(:project) } @@ -2946,40 +2989,64 @@ describe MergeRequest do end describe '#rebase_in_progress?' do - shared_examples 'checking whether a rebase is in progress' do - let(:repo_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - subject.source_project.repository.path - end - end - let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } + where(:rebase_jid, :jid_valid, :result) do + 'foo' | true | true + 'foo' | false | false + '' | true | false + nil | true | false + end - before do - system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) + with_them do + let(:merge_request) { create(:merge_request) } + + subject { merge_request.rebase_in_progress? } + + it do + # Stub out the legacy gitaly implementation + allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false } + + allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid } + + merge_request.rebase_jid = rebase_jid + + is_expected.to eq(result) end + end + end - it 'returns true when there is a current rebase directory' do - expect(subject.rebase_in_progress?).to be_truthy + describe '#gitaly_rebase_in_progress?' do + let(:repo_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.source_project.repository.path end + end + let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } - it 'returns false when there is no rebase directory' do - FileUtils.rm_rf(rebase_path) + before do + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) + end - expect(subject.rebase_in_progress?).to be_falsey - end + it 'returns true when there is a current rebase directory' do + expect(subject.rebase_in_progress?).to be_truthy + end - it 'returns false when the rebase directory has expired' do - time = 20.minutes.ago.to_time - File.utime(time, time, rebase_path) + it 'returns false when there is no rebase directory' do + FileUtils.rm_rf(rebase_path) - expect(subject.rebase_in_progress?).to be_falsey - end + expect(subject.rebase_in_progress?).to be_falsey + end - it 'returns false when the source project has been removed' do - allow(subject).to receive(:source_project).and_return(nil) + it 'returns false when the rebase directory has expired' do + time = 20.minutes.ago.to_time + File.utime(time, time, rebase_path) - expect(subject.rebase_in_progress?).to be_falsey - end + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) + + expect(subject.rebase_in_progress?).to be_falsey end end @@ -3153,4 +3220,34 @@ describe MergeRequest do it { is_expected.to be_truthy } end end + + describe '#cleanup_refs' do + subject { merge_request.cleanup_refs(only: only) } + + let(:merge_request) { build(:merge_request) } + + context 'when removing all refs' do + let(:only) { :all } + + it 'deletes all refs from the target project' do + expect(merge_request.target_project.repository) + .to receive(:delete_refs) + .with(merge_request.ref_path, merge_request.merge_ref_path, merge_request.train_ref_path) + + subject + end + end + + context 'when removing only train ref' do + let(:only) { :train } + + it 'deletes train ref from the target project' do + expect(merge_request.target_project.repository) + .to receive(:delete_refs) + .with(merge_request.train_ref_path) + + subject + end + end + end end diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb index 8ed0248e1b2..0f1283717e0 100644 --- a/spec/models/namespace/aggregation_schedule_spec.rb +++ b/spec/models/namespace/aggregation_schedule_spec.rb @@ -53,10 +53,39 @@ RSpec.describe Namespace::AggregationSchedule, :clean_gitlab_redis_shared_state, expect(Namespaces::RootStatisticsWorker) .to receive(:perform_in).once - .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id ) + .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id) aggregation_schedule.save! end + + it 'does not release the lease' do + stub_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT) + + aggregation_schedule.save! + + exclusive_lease = aggregation_schedule.exclusive_lease + expect(exclusive_lease.exists?).to be_truthy + end + + it 'only executes the workers once' do + # Avoid automatic deletion of Namespace::AggregationSchedule + # for testing purposes. + expect(Namespaces::RootStatisticsWorker) + .to receive(:perform_async).once + .and_return(nil) + + expect(Namespaces::RootStatisticsWorker) + .to receive(:perform_in).once + .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id) + .and_return(nil) + + # Scheduling workers for the first time + aggregation_schedule.schedule_root_storage_statistics + + # Executing again, this time workers should not be scheduled + # due to the lease not been released. + aggregation_schedule.schedule_root_storage_statistics + end end context 'with a personalized lease timeout' do diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 22df19d943f..a771d1bf27f 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -101,6 +101,15 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do is_expected.to eq(:error) end + Gitlab::HTTP::HTTP_ERRORS.each do |http_error| + it "sets commit status to :error with a #{http_error.name} error" do + WebMock.stub_request(:get, commit_status_path) + .to_raise(http_error) + + is_expected.to eq(:error) + end + end + { "killed" => :canceled, "failure" => :failed, diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 5d7d6c34e67..d33bbb0470f 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -142,235 +142,6 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end end - describe '#kubernetes_namespace_for' do - subject { service.kubernetes_namespace_for(project) } - - shared_examples 'a correctly formatted namespace' do - it 'returns a valid Kubernetes namespace name' do - expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex) - expect(subject).to eq(expected_namespace) - end - end - - it_behaves_like 'a correctly formatted namespace' do - let(:expected_namespace) { service.send(:default_namespace) } - end - - context 'when the project path contains forbidden characters' do - before do - project.path = '-a_Strange.Path--forSure' - end - - it_behaves_like 'a correctly formatted namespace' do - let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" } - end - end - - context 'when namespace is specified' do - before do - service.namespace = 'my-namespace' - end - - it_behaves_like 'a correctly formatted namespace' do - let(:expected_namespace) { 'my-namespace' } - end - end - - context 'when service is not assigned to project' do - before do - service.project = nil - end - - it 'does not return namespace' do - is_expected.to be_nil - end - end - end - - describe '#test' do - let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } - - before do - stub_kubeclient_discover(service.api_url) - end - - context 'with path prefix in api_url' do - let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } - - it 'tests with the prefix' do - service.api_url = 'https://kubernetes.example.com/prefix' - stub_kubeclient_discover(service.api_url) - - expect(service.test[:success]).to be_truthy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - - context 'with custom CA certificate' do - it 'is added to the certificate store' do - service.ca_pem = "CA PEM DATA" - - cert = double("certificate") - expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert) - expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) - - expect(service.test[:success]).to be_truthy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - - context 'success' do - it 'reads the discovery endpoint' do - expect(service.test[:success]).to be_truthy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - - context 'failure' do - it 'fails to read the discovery endpoint' do - WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404) - - expect(service.test[:success]).to be_falsy - expect(WebMock).to have_requested(:get, discovery_url).once - end - end - end - - describe '#predefined_variable' do - let(:kubeconfig) do - config_file = expand_fixture_path('config/kubeconfig.yml') - config = YAML.load(File.read(config_file)) - config.dig('users', 0, 'user')['token'] = 'token' - config.dig('contexts', 0, 'context')['namespace'] = namespace - config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = - Base64.strict_encode64('CA PEM DATA') - - YAML.dump(config) - end - - before do - subject.api_url = 'https://kube.domain.com' - subject.token = 'token' - subject.ca_pem = 'CA PEM DATA' - subject.project = project - end - - shared_examples 'setting variables' do - it 'sets the variables' do - expect(subject.predefined_variables(project: project)).to include( - { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }, - { key: 'KUBE_TOKEN', value: 'token', public: false, masked: true }, - { key: 'KUBE_NAMESPACE', value: namespace, public: true }, - { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }, - { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }, - { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true } - ) - end - end - - context 'namespace is provided' do - let(:namespace) { 'my-project' } - - before do - subject.namespace = namespace - end - - it_behaves_like 'setting variables' - end - - context 'no namespace provided' do - let(:namespace) { subject.kubernetes_namespace_for(project) } - - it_behaves_like 'setting variables' - - it 'sets the KUBE_NAMESPACE' do - kube_namespace = subject.predefined_variables(project: project).find { |h| h[:key] == 'KUBE_NAMESPACE' } - - expect(kube_namespace).not_to be_nil - expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/) - end - end - end - - describe '#terminals' do - let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") } - - subject { service.terminals(environment) } - - context 'with invalid pods' do - it 'returns no terminals' do - stub_reactive_cache(service, pods: [{ "bad" => "pod" }]) - - is_expected.to be_empty - end - end - - context 'with valid pods' do - let(:pod) { kube_pod(environment_slug: environment.slug, namespace: service.kubernetes_namespace_for(project), project_slug: project.full_path_slug) } - let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } - let(:terminals) { kube_terminals(service, pod) } - - before do - stub_reactive_cache( - service, - pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] - ) - end - - it 'returns terminals' do - is_expected.to eq(terminals + terminals) - end - - it 'uses max session time from settings' do - stub_application_setting(terminal_max_session_time: 600) - - times = subject.map { |terminal| terminal[:max_session_time] } - expect(times).to eq [600, 600, 600, 600] - end - end - end - - describe '#calculate_reactive_cache' do - subject { service.calculate_reactive_cache } - - let(:namespace) { service.kubernetes_namespace_for(project) } - - context 'when service is inactive' do - before do - service.active = false - end - - it { is_expected.to be_nil } - end - - context 'when kubernetes responds with valid pods' do - before do - stub_kubeclient_pods(namespace) - stub_kubeclient_deployments(namespace) # Used by EE - end - - it { is_expected.to include(pods: [kube_pod]) } - end - - context 'when kubernetes responds with 500s' do - before do - stub_kubeclient_pods(namespace, status: 500) - stub_kubeclient_deployments(namespace, status: 500) # Used by EE - end - - it { expect { subject }.to raise_error(Kubeclient::HttpError) } - end - - context 'when kubernetes responds with 404s' do - before do - stub_kubeclient_pods(namespace, status: 404) - stub_kubeclient_deployments(namespace, status: 404) # Used by EE - end - - it { is_expected.to include(pods: []) } - end - end - describe "#deprecated?" do let(:kubernetes_service) { create(:kubernetes_service) } diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 1cb49d83ffa..db3e4902c64 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -135,6 +135,49 @@ describe ProjectStatistics do expect(statistics.wiki_size).to eq(0) end end + + context 'when the column is namespace relatable' do + let(:namespace) { create(:group) } + let(:project) { create(:project, namespace: namespace) } + + context 'when the feature flag is off' do + it 'does not schedule the aggregation worker' do + stub_feature_flags(update_statistics_namespace: false, namespace: namespace) + + expect(Namespaces::ScheduleAggregationWorker) + .not_to receive(:perform_async) + + statistics.refresh!(only: [:lfs_objects_size]) + end + end + + context 'when the feature flag is on' do + it 'schedules the aggregation worker' do + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async) + + statistics.refresh!(only: [:lfs_objects_size]) + end + end + + context 'when no argument is passed' do + it 'schedules the aggregation worker' do + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async) + + statistics.refresh! + end + end + end + + context 'when the column is not namespace relatable' do + it 'does not schedules an aggregation worker' do + expect(Namespaces::ScheduleAggregationWorker) + .not_to receive(:perform_async) + + statistics.refresh!(only: [:commit_count]) + end + end end describe '#update_commit_count' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 13da7bd7407..3d967aa4ab8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1420,12 +1420,13 @@ describe Repository do source_project: project) end - it 'writes merge of source and target to MR merge_ref_path' do + it 'writes merge of source SHA and first parent ref to MR merge_ref_path' do merge_commit_id = repository.merge_to_ref(user, merge_request.diff_head_sha, merge_request, merge_request.merge_ref_path, - 'Custom message') + 'Custom message', + merge_request.target_branch_ref) merge_commit = repository.commit(merge_commit_id) diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 656d6f8b50b..54401ec4085 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -6,16 +6,6 @@ describe 'GraphQL' do let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) } - context 'graphql is disabled by feature flag' do - before do - stub_feature_flags(graphql: false) - end - - it 'does not generate a route for GraphQL' do - expect { post_graphql(query) }.to raise_error(ActionController::RoutingError) - end - end - context 'logging' do shared_examples 'logging a graphql query' do let(:expected_params) do @@ -131,4 +121,35 @@ describe 'GraphQL' do end end end + + describe 'testing for Gitaly calls' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + let(:query) do + graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)) + end + + before do + project.add_developer(user) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: user) + end + end + + context 'when Gitaly is called' do + before do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(1, 2) + end + + it "logs a warning that the 'calls_gitaly' field declaration is missing" do + expect(Gitlab::Sentry).to receive(:track_exception).once + + post_graphql(query, current_user: user) + end + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a82ecb4fd63..ced853caab4 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2033,6 +2033,9 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(202) expect(RebaseWorker.jobs.size).to eq(1) + + expect(merge_request.reload).to be_rebase_in_progress + expect(json_response['rebase_in_progress']).to be(true) end it 'returns 403 if the user cannot push to the branch' do @@ -2043,6 +2046,16 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(403) end + + it 'returns 409 if a rebase is already in progress' do + Sidekiq::Testing.fake! do + merge_request.rebase_async(user.id) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user) + end + + expect(response).to have_gitlab_http_status(409) + end end describe 'Time tracking' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index df4758f362b..a2aae257352 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -2482,7 +2482,7 @@ describe API::Projects do let(:housekeeping) { Projects::HousekeepingService.new(project) } before do - allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping) + allow(Projects::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping) end context 'when authenticated as owner' do diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 5548e3fd01a..f5ce3a3570e 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -584,6 +584,34 @@ describe API::Runners do end end + context 'when valid order_by is provided' do + context 'when sort order is not specified' do + it 'return jobs in descending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_5.id) + end + end + + context 'when sort order is specified as asc' do + it 'return jobs sorted in ascending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_4.id) + end + end + end + context 'when invalid status is provided' do it 'return 400' do get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin) @@ -591,6 +619,22 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end end + + context 'when invalid order_by is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when invalid sort is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end end context "when runner doesn't exist" do diff --git a/spec/requests/api/user_counts_spec.rb b/spec/requests/api/user_counts_spec.rb new file mode 100644 index 00000000000..c833bd047e2 --- /dev/null +++ b/spec/requests/api/user_counts_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::UserCounts do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, title: "Test") } + + describe 'GET /user_counts' do + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/user_counts') + + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'returns open counts for current user' do + get api('/user_counts', user) + + expect(response.status).to eq(200) + expect(json_response).to be_a Hash + expect(json_response['merge_requests']).to eq(1) + end + + it 'updates the mr count when a new mr is assigned' do + create(:merge_request, source_project: project, author: user, assignees: [user]) + + get api('/user_counts', user) + + expect(response.status).to eq(200) + expect(json_response).to be_a Hash + expect(json_response['merge_requests']).to eq(2) + end + end + end +end diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb deleted file mode 100644 index 3c48ead4ff2..00000000000 --- a/spec/routing/api_routing_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe 'api', 'routing' do - context 'when graphql is disabled' do - before do - stub_feature_flags(graphql: false) - end - - it 'does not route to the GraphqlController' do - expect(post('/api/graphql')).not_to route_to('graphql#execute') - end - end - - context 'when graphql is enabled' do - before do - stub_feature_flags(graphql: true) - end - - it 'routes to the GraphqlController' do - expect(post('/api/graphql')).to route_to('graphql#execute') - end - end -end diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 8a6a38fe5f8..f421432e8d6 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } - let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) } + let(:environment_status) { EnvironmentStatus.new(project, environment, merge_request, merge_request.diff_head_sha) } let(:entity) { described_class.new(environment_status, request: request) } subject { entity.as_json } @@ -55,8 +55,14 @@ describe EnvironmentStatusEntity do before do project.add_maintainer(user) allow(deployment).to receive(:prometheus_adapter).and_return(prometheus_adapter) - allow(prometheus_adapter).to receive(:query).with(:deployment, deployment).and_return(simple_metrics) allow(entity).to receive(:deployment).and_return(deployment) + + expect_next_instance_of(DeploymentMetrics) do |deployment_metrics| + allow(deployment_metrics).to receive(:prometheus_adapter).and_return(prometheus_adapter) + + allow(prometheus_adapter).to receive(:query) + .with(:deployment, deployment).and_return(simple_metrics) + end end context 'when deployment succeeded' do diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb index 986c9feb07b..cc5f086ca4e 100644 --- a/spec/serializers/test_case_entity_spec.rb +++ b/spec/serializers/test_case_entity_spec.rb @@ -24,7 +24,7 @@ describe TestCaseEntity do it 'contains correct test case details' do expect(subject[:status]).to eq('failed') - expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary') + expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary') expect(subject[:classname]).to eq('spec.test_spec') expect(subject[:execution_time]).to eq(2.22) end diff --git a/spec/serializers/test_reports_comparer_entity_spec.rb b/spec/serializers/test_reports_comparer_entity_spec.rb index 59c058fe368..4a951bbbde4 100644 --- a/spec/serializers/test_reports_comparer_entity_spec.rb +++ b/spec/serializers/test_reports_comparer_entity_spec.rb @@ -53,13 +53,7 @@ describe TestReportsComparerEntity do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) - end - - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) end it 'contains correct compared test reports details' do diff --git a/spec/serializers/test_reports_comparer_serializer_spec.rb b/spec/serializers/test_reports_comparer_serializer_spec.rb index 9ea86c0dd83..62dc6f486c5 100644 --- a/spec/serializers/test_reports_comparer_serializer_spec.rb +++ b/spec/serializers/test_reports_comparer_serializer_spec.rb @@ -44,13 +44,7 @@ describe TestReportsComparerSerializer do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) - end - - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) end it 'matches the schema' do diff --git a/spec/serializers/test_suite_comparer_entity_spec.rb b/spec/serializers/test_suite_comparer_entity_spec.rb index f61331f53a0..4b2cca2c68c 100644 --- a/spec/serializers/test_suite_comparer_entity_spec.rb +++ b/spec/serializers/test_suite_comparer_entity_spec.rb @@ -11,16 +11,10 @@ describe TestSuiteComparerEntity do let(:test_case_success) { create_test_case_rspec_success } let(:test_case_failed) { create_test_case_rspec_failed } - let(:test_case_resolved) do - create_test_case_rspec_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end - end - describe '#as_json' do subject { entity.as_json } - context 'when head sutie has a newly failed test case which does not exist in base' do + context 'when head suite has a newly failed test case which does not exist in base' do before do base_suite.add_test_case(test_case_success) head_suite.add_test_case(test_case_failed) @@ -41,7 +35,7 @@ describe TestSuiteComparerEntity do end end - context 'when head sutie still has a failed test case which failed in base' do + context 'when head suite still has a failed test case which failed in base' do before do base_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed) @@ -62,10 +56,10 @@ describe TestSuiteComparerEntity do end end - context 'when head sutie has a success test case which failed in base' do + context 'when head suite has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'contains correct compared test suite details' do @@ -74,13 +68,83 @@ describe TestSuiteComparerEntity do expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0) expect(subject[:new_failures]).to be_empty subject[:resolved_failures].first.tap do |resolved_failure| - expect(resolved_failure[:status]).to eq(test_case_resolved.status) - expect(resolved_failure[:name]).to eq(test_case_resolved.name) - expect(resolved_failure[:execution_time]).to eq(test_case_resolved.execution_time) - expect(resolved_failure[:system_output]).to eq(test_case_resolved.system_output) + expect(resolved_failure[:status]).to eq(test_case_success.status) + expect(resolved_failure[:name]).to eq(test_case_success.name) + expect(resolved_failure[:execution_time]).to eq(test_case_success.execution_time) + expect(resolved_failure[:system_output]).to eq(test_case_success.system_output) end expect(subject[:existing_failures]).to be_empty end end + + context 'limits amount of tests returned' do + before do + stub_const('TestSuiteComparerEntity::DEFAULT_MAX_TESTS', 2) + stub_const('TestSuiteComparerEntity::DEFAULT_MIN_TESTS', 1) + end + + context 'prefers new over existing and resolved' do + before do + 3.times { add_new_failure } + 3.times { add_existing_failure } + 3.times { add_resolved_failure } + end + + it 'returns 2 new failures, and 1 of resolved and existing' do + expect(subject[:summary]).to include(total: 9, resolved: 3, failed: 6) + expect(subject[:new_failures].count).to eq(2) + expect(subject[:existing_failures].count).to eq(1) + expect(subject[:resolved_failures].count).to eq(1) + end + end + + context 'prefers existing over resolved' do + before do + 3.times { add_existing_failure } + 3.times { add_resolved_failure } + end + + it 'returns 2 existing failures, and 1 resolved' do + expect(subject[:summary]).to include(total: 6, resolved: 3, failed: 3) + expect(subject[:new_failures].count).to eq(0) + expect(subject[:existing_failures].count).to eq(2) + expect(subject[:resolved_failures].count).to eq(1) + end + end + + context 'limits amount of resolved' do + before do + 3.times { add_resolved_failure } + end + + it 'returns 2 resolved failures' do + expect(subject[:summary]).to include(total: 3, resolved: 3, failed: 0) + expect(subject[:new_failures].count).to eq(0) + expect(subject[:existing_failures].count).to eq(0) + expect(subject[:resolved_failures].count).to eq(2) + end + end + + private + + def add_new_failure + failed_case = create_test_case_rspec_failed(SecureRandom.hex) + head_suite.add_test_case(failed_case) + end + + def add_existing_failure + failed_case = create_test_case_rspec_failed(SecureRandom.hex) + base_suite.add_test_case(failed_case) + head_suite.add_test_case(failed_case) + end + + def add_resolved_failure + case_name = SecureRandom.hex + failed_case = create_test_case_rspec_failed(case_name) + success_case = create_test_case_rspec_success(case_name) + base_suite.add_test_case(failed_case) + head_suite.add_test_case(success_case) + end + end end end diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index cd08e0b6f32..a409f21a7e4 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -59,6 +59,11 @@ describe AutoMerge::BaseService do context 'when strategy is merge when pipeline succeeds' do let(:service) { AutoMerge::MergeWhenPipelineSucceedsService.new(project, user) } + before do + pipeline = build(:ci_pipeline) + allow(merge_request).to receive(:actual_head_pipeline) { pipeline } + end + it 'sets the auto merge strategy' do subject @@ -116,11 +121,7 @@ describe AutoMerge::BaseService do end end - describe '#cancel' do - subject { service.cancel(merge_request) } - - let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } - + shared_examples_for 'Canceled or Dropped' do it 'removes properies from the merge request' do subject @@ -168,6 +169,20 @@ describe AutoMerge::BaseService do it 'does not yield block' do expect { |b| service.execute(merge_request, &b) }.not_to yield_control end + end + end + + describe '#cancel' do + subject { service.cancel(merge_request) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + + it_behaves_like 'Canceled or Dropped' + + context 'when failed to save' do + before do + allow(merge_request).to receive(:save) { false } + end it 'returns error status' do expect(subject[:status]).to eq(:error) @@ -175,4 +190,24 @@ describe AutoMerge::BaseService do end end end + + describe '#abort' do + subject { service.abort(merge_request, reason) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let(:reason) { 'an error'} + + it_behaves_like 'Canceled or Dropped' + + context 'when failed to save' do + before do + allow(merge_request).to receive(:save) { false } + end + + it 'returns error status' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq("Can't abort the automatic merge") + end + end + end end diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb index a20bf8e17e4..931b52470c4 100644 --- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb @@ -64,8 +64,11 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do end it 'creates a system note' do + pipeline = build(:ci_pipeline) + allow(merge_request).to receive(:actual_head_pipeline) { pipeline } + note = merge_request.notes.last - expect(note.note).to match %r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{8}} + expect(note.note).to match "enabled an automatic merge when the pipeline for #{pipeline.sha}" end end @@ -174,6 +177,17 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do end end + describe "#abort" do + before do + service.abort(mr_merge_if_green_enabled, 'an error') + end + + it 'posts a system note' do + note = mr_merge_if_green_enabled.notes.last + expect(note.note).to include 'aborted the automatic merge' + end + end + describe 'pipeline integration' do context 'when there are multiple stages in the pipeline' do let(:ref) { mr_merge_if_green_enabled.source_branch } diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb index 93a22e60498..50dfc49a59c 100644 --- a/spec/services/auto_merge_service_spec.rb +++ b/spec/services/auto_merge_service_spec.rb @@ -161,4 +161,29 @@ describe AutoMergeService do end end end + + describe '#abort' do + subject { service.abort(merge_request, error) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let(:error) { 'an error' } + + it 'delegates to a relevant service instance' do + expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service| + expect(service).to receive(:abort).with(merge_request, error) + end + + subject + end + + context 'when auto merge is not enabled' do + let(:merge_request) { create(:merge_request) } + + it 'returns error' do + expect(subject[:message]).to eq("Can't abort the automatic merge") + expect(subject[:status]).to eq(:error) + expect(subject[:http_status]).to eq(406) + end + end + end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index aebc5ba2874..3d2d4b5f216 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -11,343 +11,371 @@ describe Issuable::BulkUpdateService do .reverse_merge(issuable_ids: Array(issuables).map(&:id).join(',')) type = Array(issuables).first.model_name.param_key - Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type) + Issuable::BulkUpdateService.new(user, bulk_update_params).execute(type) end - describe 'close issues' do - let(:issues) { create_list(:issue, 2, project: project) } - - it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'close') + shared_examples 'updates milestones' do + it 'succeeds' do + result = bulk_update(issues, milestone_id: milestone.id) expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end - it 'closes all the issues passed' do - bulk_update(issues, state_event: 'close') + it 'updates the issues milestone' do + bulk_update(issues, milestone_id: milestone.id) - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty + issues.each do |issue| + expect(issue.reload.milestone).to eq(milestone) + end end + end - context 'when issue for a different project is created' do - let(:private_project) { create(:project, :private) } - let(:issue) { create(:issue, project: private_project, author: user) } + context 'with project issues' do + describe 'close issues' do + let(:issues) { create_list(:issue, 2, project: project) } - context 'when user has access to the project' do - it 'closes all issues passed' do - private_project.add_maintainer(user) + it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'close') - bulk_update(issues + [issue], state_event: 'close') + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(issues.count) + end - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty - expect(private_project.issues.closed).not_to be_empty - end + it 'closes all the issues passed' do + bulk_update(issues, state_event: 'close') + + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty end - context 'when user does not have access to project' do - it 'only closes all issues that the user has access to' do - bulk_update(issues + [issue], state_event: 'close') + context 'when issue for a different project is created' do + let(:private_project) { create(:project, :private) } + let(:issue) { create(:issue, project: private_project, author: user) } + + context 'when user has access to the project' do + it 'closes all issues passed' do + private_project.add_maintainer(user) + + bulk_update(issues + [issue], state_event: 'close') + + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + expect(private_project.issues.closed).not_to be_empty + end + end + + context 'when user does not have access to project' do + it 'only closes all issues that the user has access to' do + bulk_update(issues + [issue], state_event: 'close') - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty - expect(private_project.issues.closed).to be_empty + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + expect(private_project.issues.closed).to be_empty + end end end end - end - describe 'reopen issues' do - let(:issues) { create_list(:closed_issue, 2, project: project) } + describe 'reopen issues' do + let(:issues) { create_list(:closed_issue, 2, project: project) } - it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'reopen') + it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'reopen') - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(issues.count) - end + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(issues.count) + end - it 'reopens all the issues passed' do - bulk_update(issues, state_event: 'reopen') + it 'reopens all the issues passed' do + bulk_update(issues, state_event: 'reopen') - expect(project.issues.closed).to be_empty - expect(project.issues.opened).not_to be_empty + expect(project.issues.closed).to be_empty + expect(project.issues.opened).not_to be_empty + end end - end - describe 'updating merge request assignee' do - let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) } + describe 'updating merge request assignee' do + let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) } - context 'when the new assignee ID is a valid user' do - it 'succeeds' do - new_assignee = create(:user) - project.add_developer(new_assignee) + context 'when the new assignee ID is a valid user' do + it 'succeeds' do + new_assignee = create(:user) + project.add_developer(new_assignee) - result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id]) + result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id]) - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - end + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end - it 'updates the assignee to the user ID passed' do - assignee = create(:user) - project.add_developer(assignee) + it 'updates the assignee to the user ID passed' do + assignee = create(:user) + project.add_developer(assignee) - expect { bulk_update(merge_request, assignee_ids: [assignee.id]) } - .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id]) + expect { bulk_update(merge_request, assignee_ids: [assignee.id]) } + .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id]) + end end - end - context "when the new assignee ID is #{IssuableFinder::NONE}" do - it 'unassigns the issues' do - expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) } - .to change { merge_request.reload.assignee_ids }.to([]) + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it 'unassigns the issues' do + expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) } + .to change { merge_request.reload.assignee_ids }.to([]) + end end - end - context 'when the new assignee ID is not present' do - it 'does not unassign' do - expect { bulk_update(merge_request, assignee_ids: []) } - .not_to change { merge_request.reload.assignee_ids } + context 'when the new assignee ID is not present' do + it 'does not unassign' do + expect { bulk_update(merge_request, assignee_ids: []) } + .not_to change { merge_request.reload.assignee_ids } + end end end - end - describe 'updating issue assignee' do - let(:issue) { create(:issue, project: project, assignees: [user]) } + describe 'updating issue assignee' do + let(:issue) { create(:issue, project: project, assignees: [user]) } - context 'when the new assignee ID is a valid user' do - it 'succeeds' do - new_assignee = create(:user) - project.add_developer(new_assignee) + context 'when the new assignee ID is a valid user' do + it 'succeeds' do + new_assignee = create(:user) + project.add_developer(new_assignee) - result = bulk_update(issue, assignee_ids: [new_assignee.id]) + result = bulk_update(issue, assignee_ids: [new_assignee.id]) - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - end + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end - it 'updates the assignee to the user ID passed' do - assignee = create(:user) - project.add_developer(assignee) - expect { bulk_update(issue, assignee_ids: [assignee.id]) } - .to change { issue.reload.assignees.first }.from(user).to(assignee) + it 'updates the assignee to the user ID passed' do + assignee = create(:user) + project.add_developer(assignee) + expect { bulk_update(issue, assignee_ids: [assignee.id]) } + .to change { issue.reload.assignees.first }.from(user).to(assignee) + end end - end - context "when the new assignee ID is #{IssuableFinder::NONE}" do - it "unassigns the issues" do - expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) } - .to change { issue.reload.assignees.count }.from(1).to(0) + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it "unassigns the issues" do + expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) } + .to change { issue.reload.assignees.count }.from(1).to(0) + end end - end - context 'when the new assignee ID is not present' do - it 'does not unassign' do - expect { bulk_update(issue, assignee_ids: []) } - .not_to change { issue.reload.assignees } + context 'when the new assignee ID is not present' do + it 'does not unassign' do + expect { bulk_update(issue, assignee_ids: []) } + .not_to change { issue.reload.assignees } + end end end - end - - describe 'updating milestones' do - let(:issue) { create(:issue, project: project) } - let(:milestone) { create(:milestone, project: project) } - it 'succeeds' do - result = bulk_update(issue, milestone_id: milestone.id) + describe 'updating milestones' do + let(:issues) { [create(:issue, project: project)] } + let(:milestone) { create(:milestone, project: project) } - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) + it_behaves_like 'updates milestones' end - it 'updates the issue milestone' do - expect { bulk_update(issue, milestone_id: milestone.id) } - .to change { issue.reload.milestone }.from(nil).to(milestone) - end - end - - describe 'updating labels' do - def create_issue_with_labels(labels) - create(:labeled_issue, project: project, labels: labels) - end + describe 'updating labels' do + def create_issue_with_labels(labels) + create(:labeled_issue, project: project, labels: labels) + end - let(:bug) { create(:label, project: project) } - let(:regression) { create(:label, project: project) } - let(:merge_requests) { create(:label, project: project) } - - let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } - let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } - let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } - let(:issue_no_labels) { create(:issue, project: project) } - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } - - let(:labels) { [] } - let(:add_labels) { [] } - let(:remove_labels) { [] } - - let(:bulk_update_params) do - { - label_ids: labels.map(&:id), - add_label_ids: add_labels.map(&:id), - remove_label_ids: remove_labels.map(&:id) - } - end + let(:bug) { create(:label, project: project) } + let(:regression) { create(:label, project: project) } + let(:merge_requests) { create(:label, project: project) } - before do - bulk_update(issues, bulk_update_params) - end + let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } + let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } + let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } + let(:issue_no_labels) { create(:issue, project: project) } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } - context 'when label_ids are passed' do - let(:issues) { [issue_all_labels, issue_no_labels] } - let(:labels) { [bug, regression] } + let(:labels) { [] } + let(:add_labels) { [] } + let(:remove_labels) { [] } - it 'updates the labels of all issues passed to the labels passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) + let(:bulk_update_params) do + { + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id) + } end - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + before do + bulk_update(issues, bulk_update_params) end - context 'when those label IDs are empty' do - let(:labels) { [] } + context 'when label_ids are passed' do + let(:issues) { [issue_all_labels, issue_no_labels] } + let(:labels) { [bug, regression] } - it 'updates the issues passed to have no labels' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + it 'updates the labels of all issues passed to the labels passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) end - end - end - context 'when add_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:add_labels) { [bug, regression, merge_requests] } + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end - it 'adds those label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) - end + context 'when those label IDs are empty' do + let(:labels) { [] } - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + it 'updates the issues passed to have no labels' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + end end - end - context 'when remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:remove_labels) { [bug, regression, merge_requests] } + context 'when add_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug, regression, merge_requests] } - it 'removes those label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) - end + it 'adds those label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) + end - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end - end - context 'when add_label_ids and remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } + context 'when remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:remove_labels) { [bug, regression, merge_requests] } - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end + it 'removes those label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - end + context 'when add_label_ids and remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } - context 'when add_label_ids and label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } - let(:labels) { [merge_requests] } - let(:add_labels) { [regression] } + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) - end + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end - it 'does not update issues not passed in' do - expect(issue_no_labels.label_ids).to be_empty - end - end + context 'when add_label_ids and label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } + let(:labels) { [merge_requests] } + let(:add_labels) { [regression] } - context 'when remove_label_ids and label_ids are passed' do - let(:issues) { [issue_no_labels, issue_bug_and_regression] } - let(:labels) { [merge_requests] } - let(:remove_labels) { [regression] } + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) + end - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) - end + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + it 'does not update issues not passed in' do + expect(issue_no_labels.label_ids).to be_empty + end end - it 'does not update issues not passed in' do - expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) - end - end + context 'when remove_label_ids and label_ids are passed' do + let(:issues) { [issue_no_labels, issue_bug_and_regression] } + let(:labels) { [merge_requests] } + let(:remove_labels) { [regression] } - context 'when add_label_ids, remove_label_ids, and label_ids are passed' do - let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } - let(:labels) { [regression] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + it 'does not update issues not passed in' do + expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) + end end - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) - end + context 'when add_label_ids, remove_label_ids, and label_ids are passed' do + let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } + let(:labels) { [regression] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end end - end - describe 'subscribe to issues' do - let(:issues) { create_list(:issue, 2, project: project) } + describe 'subscribe to issues' do + let(:issues) { create_list(:issue, 2, project: project) } - it 'subscribes the given user' do - bulk_update(issues, subscription_event: 'subscribe') + it 'subscribes the given user' do + bulk_update(issues, subscription_event: 'subscribe') - expect(issues).to all(be_subscribed(user, project)) + expect(issues).to all(be_subscribed(user, project)) + end end - end - describe 'unsubscribe from issues' do - let(:issues) do - create_list(:closed_issue, 2, project: project) do |issue| - issue.subscriptions.create(user: user, project: project, subscribed: true) + describe 'unsubscribe from issues' do + let(:issues) do + create_list(:closed_issue, 2, project: project) do |issue| + issue.subscriptions.create(user: user, project: project, subscribed: true) + end + end + + it 'unsubscribes the given user' do + bulk_update(issues, subscription_event: 'unsubscribe') + + issues.each do |issue| + expect(issue).not_to be_subscribed(user, project) + end end end + end - it 'unsubscribes the given user' do - bulk_update(issues, subscription_event: 'unsubscribe') + context 'with group issues' do + let(:group) { create(:group) } - issues.each do |issue| - expect(issue).not_to be_subscribed(user, project) + context 'updating milestone' do + let(:milestone) { create(:milestone, group: group) } + let(:project1) { create(:project, :repository, group: group) } + let(:project2) { create(:project, :repository, group: group) } + let(:issue1) { create(:issue, project: project1) } + let(:issue2) { create(:issue, project: project2) } + let(:issues) { [issue1, issue2] } + + before do + group.add_maintainer(user) end + + it_behaves_like 'updates milestones' end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 28fa5d12d9c..468e7c286d5 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -480,6 +480,22 @@ describe Issues::UpdateService, :mailer do update_issue(description: "- [x] Task 1\n- [X] Task 2") end + it 'does not check for spam on task status change' do + params = { + update_task: { + index: 1, + checked: false, + line_source: '- [x] Task 1', + line_number: 1 + } + } + service = described_class.new(project, user, params) + + expect(service).not_to receive(:spam_check) + + service.execute(issue) + end + it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') note2 = find_note('marked the task **Task 2** as completed') diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 61f99f82a76..14012b4ea2d 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -22,7 +22,6 @@ describe MergeRequests::MergeToRefService do shared_examples_for 'successfully merges to ref with merge method' do it 'writes commit to merge ref' do repository = project.repository - target_ref = merge_request.merge_ref_path expect(repository.ref_exists?(target_ref)).to be(false) @@ -33,7 +32,7 @@ describe MergeRequests::MergeToRefService do expect(result[:status]).to eq(:success) expect(result[:commit_id]).to be_present expect(result[:source_id]).to eq(merge_request.source_branch_sha) - expect(result[:target_id]).to eq(merge_request.target_branch_sha) + expect(result[:target_id]).to eq(repository.commit(first_parent_ref).sha) expect(repository.ref_exists?(target_ref)).to be(true) expect(ref_head.id).to eq(result[:commit_id]) end @@ -74,17 +73,22 @@ describe MergeRequests::MergeToRefService do describe '#execute' do let(:service) do - described_class.new(project, user, commit_message: 'Awesome message', - should_remove_source_branch: true) + described_class.new(project, user, **params) end + let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true } } + def process_merge_to_ref perform_enqueued_jobs do service.execute(merge_request) end end - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' context 'commit history comparison with regular MergeService' do @@ -129,14 +133,22 @@ describe MergeRequests::MergeToRefService do context 'when semi-linear merge method' do let(:merge_method) { :rebase_merge } - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' end context 'when fast-forward merge method' do let(:merge_method) { :ff } - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' end @@ -178,5 +190,43 @@ describe MergeRequests::MergeToRefService do it { expect(todo).not_to be_done } end + + context 'when target ref is passed as a parameter' do + let(:params) { { commit_message: 'merge train', target_ref: target_ref } } + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { 'refs/merge-requests/1/train' } + end + end + + describe 'cascading merge refs' do + set(:project) { create(:project, :repository) } + let(:params) { { commit_message: 'Cascading merge', first_parent_ref: first_parent_ref, target_ref: target_ref } } + + context 'when first merge happens' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'feature', + target_project: project, target_branch: 'master') + end + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { 'refs/merge-requests/1/train' } + end + + context 'when second merge happens' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'improve/awesome', + target_project: project, target_branch: 'master') + end + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/merge-requests/1/train' } + let(:target_ref) { 'refs/merge-requests/2/train' } + end + end + end + end end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index 7e2f03d1097..ee9caaf2f47 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -6,10 +6,12 @@ describe MergeRequests::RebaseService do include ProjectForksHelper let(:user) { create(:user) } + let(:rebase_jid) { 'fake-rebase-jid' } let(:merge_request) do - create(:merge_request, + create :merge_request, source_branch: 'feature_conflict', - target_branch: 'master') + target_branch: 'master', + rebase_jid: rebase_jid end let(:project) { merge_request.project } let(:repository) { project.repository.raw } @@ -23,11 +25,11 @@ describe MergeRequests::RebaseService do describe '#execute' do context 'when another rebase is already in progress' do before do - allow(merge_request).to receive(:rebase_in_progress?).and_return(true) + allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true) end it 'saves the error message' do - subject.execute(merge_request) + service.execute(merge_request) expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress' end @@ -36,6 +38,13 @@ describe MergeRequests::RebaseService do expect(service.execute(merge_request)).to match(status: :error, message: described_class::REBASE_ERROR) end + + it 'clears rebase_jid' do + expect { service.execute(merge_request) } + .to change { merge_request.rebase_jid } + .from(rebase_jid) + .to(nil) + end end shared_examples 'sequence of failure and success' do @@ -43,14 +52,19 @@ describe MergeRequests::RebaseService do allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong') service.execute(merge_request) + merge_request.reload - expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR) + expect(merge_request.rebase_jid).to eq(nil) allow(repository).to receive(:gitaly_operation_client).and_call_original + merge_request.update!(rebase_jid: rebase_jid) service.execute(merge_request) + merge_request.reload - expect(merge_request.reload.merge_error).to eq nil + expect(merge_request.merge_error).to eq(nil) + expect(merge_request.rebase_jid).to eq(nil) end end @@ -72,7 +86,7 @@ describe MergeRequests::RebaseService do it 'saves a generic error message' do subject.execute(merge_request) - expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR) end it 'returns an error' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 2a2547f9400..157cfc46e69 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -332,7 +332,7 @@ describe SystemNoteService do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) } + subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, pipeline.sha) } it_behaves_like 'a system note' do let(:action) { 'merge' } @@ -359,6 +359,22 @@ describe SystemNoteService do end end + describe '.abort_merge_when_pipeline_succeeds' do + let(:noteable) do + create(:merge_request, source_project: project, target_project: project) + end + + subject { described_class.abort_merge_when_pipeline_succeeds(noteable, project, author, 'merge request was closed') } + + it_behaves_like 'a system note' do + let(:action) { 'merge' } + end + + it "posts the 'merge when pipeline succeeds' system note" do + expect(subject.note).to eq "aborted the automatic merge because merge request was closed" + end + end + describe '.change_title' do let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') } @@ -1175,16 +1191,30 @@ describe SystemNoteService do end it 'links to the diff in the system note' do - expect(subject.note).to include('version 1') - diff_id = merge_request.merge_request_diff.id line_code = change_position.line_code(project.repository) - expect(subject.note).to include(diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code)) + link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code) + + expect(subject.note).to eq("changed this line in [version 1 of the diff](#{link})") + end + + context 'discussion is on an image' do + let(:discussion) { create(:image_diff_note_on_merge_request, project: project).to_discussion } + + it 'links to the diff in the system note' do + diff_id = merge_request.merge_request_diff.id + file_hash = change_position.file_hash + link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: file_hash) + + expect(subject.note).to eq("changed this file in [version 1 of the diff](#{link})") + end end end - context 'when the change_position is invalid for the discussion' do - let(:change_position) { project.commit(sample_commit.id) } + context 'when the change_position does not point to a valid version' do + before do + allow(merge_request).to receive(:version_params_for).and_return(nil) + end it 'creates a new note in the discussion' do # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. diff --git a/spec/support/helpers/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb index d32bc2424c0..fb2a110422a 100644 --- a/spec/support/helpers/devise_helpers.rb +++ b/spec/support/helpers/devise_helpers.rb @@ -21,4 +21,16 @@ module DeviseHelpers context.env end end + + def with_omniauth_full_host(&block) + # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` + # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and + # anything after it) from the request URI. + omniauth_config_full_host = OmniAuth.config.full_host + OmniAuth.config.full_host = ->(request) { ActionDispatch::Request.new(request).base_url } + + yield + + OmniAuth.config.full_host = omniauth_config_full_host + end end diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb index a7605cd483a..22cd8152d77 100644 --- a/spec/support/helpers/fake_u2f_device.rb +++ b/spec/support/helpers/fake_u2f_device.rb @@ -32,6 +32,10 @@ class FakeU2fDevice ") end + def fake_u2f_authentication + @page.execute_script("window.gl.u2fAuthenticate.renderAuthenticated('abc');") + end + private def u2f_device(app_id) diff --git a/spec/support/helpers/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb index cd49bb148f2..c83860d7b51 100644 --- a/spec/support/helpers/git_http_helpers.rb +++ b/spec/support/helpers/git_http_helpers.rb @@ -1,4 +1,8 @@ +require_relative 'workhorse_helpers' + module GitHttpHelpers + include WorkhorseHelpers + def clone_get(project, options = {}) get "/#{project}/info/refs", params: { service: 'git-upload-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token)) end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 0bb2d2510c2..0cb99b4e087 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -87,12 +87,17 @@ module LoginHelpers click_link "oauth-login-#{provider}" end + def fake_successful_u2f_authentication + allow(U2fRegistration).to receive(:authenticate).and_return(true) + FakeU2fDevice.new(page, nil).fake_u2f_authentication + end + def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response) response_object = { document: saml_xml(saml_response) } mock_auth_hash(provider, uid, email, response_object: response_object) end - def mock_auth_hash(provider, uid, email, response_object: nil) + def configure_mock_auth(provider, uid, email, response_object: nil) # The mock_auth configuration allows you to set per-provider (or default) # authentication hashes to return during integration testing. OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ @@ -118,6 +123,11 @@ module LoginHelpers response_object: response_object } }) + end + + def mock_auth_hash(provider, uid, email, response_object: nil) + configure_mock_auth(provider, uid, email, response_object: response_object) + original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth'] Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] diff --git a/spec/support/helpers/position_tracer_helpers.rb b/spec/support/helpers/position_tracer_helpers.rb new file mode 100644 index 00000000000..bbf6e06dd40 --- /dev/null +++ b/spec/support/helpers/position_tracer_helpers.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module PositionTracerHelpers + def diff_refs(base_commit, head_commit) + Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) + end + + def position(attrs = {}) + attrs.reverse_merge!( + diff_refs: old_diff_refs + ) + Gitlab::Diff::Position.new(attrs) + end + + def expect_new_position(attrs, result = subject) + aggregate_failures("expect new position #{attrs.inspect}") do + if attrs.nil? + expect(result[:outdated]).to be_truthy + else + new_position = result[:position] + + expect(result[:outdated]).to be_falsey + expect(new_position).not_to be_nil + expect(new_position.diff_refs).to eq(new_diff_refs) + + attrs.each do |attr, value| + expect(new_position.send(attr)).to eq(value) + end + end + end + end + + def expect_change_position(attrs, result = subject) + aggregate_failures("expect change position #{attrs.inspect}") do + change_position = result[:position] + + expect(result[:outdated]).to be_truthy + + if attrs.nil? || attrs.empty? + expect(change_position).to be_nil + else + expect(change_position).not_to be_nil + expect(change_position.diff_refs).to eq(change_diff_refs) + + attrs.each do |attr, value| + expect(change_position.send(attr)).to eq(value) + end + end + end + end + + def create_branch(new_name, branch_name) + CreateBranchService.new(project, current_user).execute(new_name, branch_name) + end + + def create_file(branch_name, file_name, content) + Files::CreateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Create file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def update_file(branch_name, file_name, content) + Files::UpdateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Update file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def delete_file(branch_name, file_name) + Files::DeleteService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Delete file", + file_path: file_name + ).execute + project.commit(branch_name) + end +end diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb index b76b53db0b9..528da37e8cf 100644 --- a/spec/support/helpers/reactive_caching_helpers.rb +++ b/spec/support/helpers/reactive_caching_helpers.rb @@ -10,7 +10,7 @@ module ReactiveCachingHelpers def stub_reactive_cache(subject = nil, data = nil, *qualifiers) allow(ReactiveCachingWorker).to receive(:perform_async) allow(ReactiveCachingWorker).to receive(:perform_in) - write_reactive_cache(subject, data, *qualifiers) unless data.nil? + write_reactive_cache(subject, data, *qualifiers) unless subject.nil? end def synchronous_reactive_cache(subject) diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb index 45c6e04dbf3..6840fb9a860 100644 --- a/spec/support/test_reports/test_reports_helper.rb +++ b/spec/support/test_reports/test_reports_helper.rb @@ -1,36 +1,36 @@ module TestReportsHelper - def create_test_case_rspec_success + def create_test_case_rspec_success(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 1 and b is 3 returns summary', - classname: 'spec.test_spec', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 1.11, status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) end - def create_test_case_rspec_failed + def create_test_case_rspec_failed(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( - name: 'Test#sum when a is 2 and b is 2 returns summary', - classname: 'spec.test_spec', + name: 'Test#sum when a is 1 and b is 3 returns summary', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 2.22, system_output: sample_rspec_failed_message, status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end - def create_test_case_rspec_skipped + def create_test_case_rspec_skipped(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 3 and b is 3 returns summary', - classname: 'spec.test_spec', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 3.33, status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED) end - def create_test_case_rspec_error + def create_test_case_rspec_error(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 4 and b is 4 returns summary', - classname: 'spec.test_spec', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 4.44, status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR) @@ -48,34 +48,34 @@ module TestReportsHelper EOF end - def create_test_case_java_success + def create_test_case_java_success(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'addTest', + name: name, classname: 'CalculatorTest', execution_time: 5.55, status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) end - def create_test_case_java_failed + def create_test_case_java_failed(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'subtractTest', + name: name, classname: 'CalculatorTest', execution_time: 6.66, system_output: sample_java_failed_message, status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end - def create_test_case_java_skipped + def create_test_case_java_skipped(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'multiplyTest', + name: name, classname: 'CalculatorTest', execution_time: 7.77, status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED) end - def create_test_case_java_error + def create_test_case_java_error(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'divideTest', + name: name, classname: 'CalculatorTest', execution_time: 8.88, status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR) diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index a9e03f3d4e5..5ee0a10f38d 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -85,8 +85,7 @@ describe FileMover do context 'when tmp uploader is not local storage' do before do - allow(PersonalFileUploader).to receive(:object_store_enabled?) { true } - tmp_uploader.object_store = ObjectStorage::Store::REMOTE + stub_uploads_object_storage(uploader: PersonalFileUploader) allow_any_instance_of(PersonalFileUploader).to receive(:file_storage?) { false } end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 185c62491ce..04206de3dc6 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -184,40 +184,37 @@ describe FileUploader do end end - describe '#cache!' do - subject do - uploader.store!(uploaded_file) - end + context 'when remote file is used' do + let(:temp_file) { Tempfile.new("test") } - context 'when remote file is used' do - let(:temp_file) { Tempfile.new("test") } + let!(:fog_connection) do + stub_uploads_object_storage(described_class) + end - let!(:fog_connection) do - stub_uploads_object_storage(described_class) - end + let(:filename) { "my file.txt" } + let(:uploaded_file) do + UploadedFile.new(temp_file.path, filename: filename, remote_id: "test/123123") + end - let(:uploaded_file) do - UploadedFile.new(temp_file.path, filename: "my file.txt", remote_id: "test/123123") - end + let!(:fog_file) do + fog_connection.directories.new(key: 'uploads').files.create( + key: 'tmp/uploads/test/123123', + body: 'content' + ) + end - let!(:fog_file) do - fog_connection.directories.new(key: 'uploads').files.create( - key: 'tmp/uploads/test/123123', - body: 'content' - ) - end + before do + FileUtils.touch(temp_file) - before do - FileUtils.touch(temp_file) - end + uploader.store!(uploaded_file) + end - after do - FileUtils.rm_f(temp_file) - end + after do + FileUtils.rm_f(temp_file) + end + describe '#cache!' do it 'file is stored remotely in permament location with sanitized name' do - subject - expect(uploader).to be_exists expect(uploader).not_to be_cached expect(uploader).not_to be_file_storage @@ -228,5 +225,18 @@ describe FileUploader do expect(uploader.object_store).to eq(described_class::Store::REMOTE) end end + + describe '#to_h' do + subject { uploader.to_h } + + let(:filename) { 'my+file.txt' } + + it 'generates URL using original file name instead of filename returned by object storage' do + # GCS returns a URL with a `+` instead of `%2B` + allow(uploader.file).to receive(:url).and_return('https://storage.googleapis.com/gitlab-test-uploads/@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/64c5065e62100b1a12841644256a98be/my+file.txt') + + expect(subject[:url]).to end_with(filename) + end + end end end diff --git a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb index 7432ca12f2a..d4a49a3f53a 100644 --- a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb +++ b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Namespaces::ScheduleAggregationWorker, '#perform' do +describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_redis_shared_state do let(:group) { create(:group) } subject(:worker) { described_class.new } @@ -10,6 +10,8 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do context 'when group is the root ancestor' do context 'when aggregation schedule exists' do it 'does not create a new one' do + stub_aggregation_schedule_statistics + Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: group.id) expect do @@ -18,26 +20,25 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do end end - context 'when update_statistics_namespace is off' do - it 'does not create a new one' do - stub_feature_flags(update_statistics_namespace: false, namespace: group) + context 'when aggregation schedule does not exist' do + it 'creates one' do + stub_aggregation_schedule_statistics expect do worker.perform(group.id) - end.not_to change(Namespace::AggregationSchedule, :count) + end.to change(Namespace::AggregationSchedule, :count).by(1) + + expect(group.aggregation_schedule).to be_present end end - context 'when aggregation schedule does not exist' do - it 'creates one' do - allow_any_instance_of(Namespace::AggregationSchedule) - .to receive(:schedule_root_storage_statistics).and_return(nil) + context 'when update_statistics_namespace is off' do + it 'does not create a new one' do + stub_feature_flags(update_statistics_namespace: false, namespace: group) expect do worker.perform(group.id) - end.to change(Namespace::AggregationSchedule, :count).by(1) - - expect(group.aggregation_schedule).to be_present + end.not_to change(Namespace::AggregationSchedule, :count) end end end @@ -47,8 +48,7 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do let(:group) { create(:group, parent: parent_group) } it 'creates an aggregation schedule for the root' do - allow_any_instance_of(Namespace::AggregationSchedule) - .to receive(:schedule_root_storage_statistics).and_return(nil) + stub_aggregation_schedule_statistics worker.perform(group.id) @@ -63,4 +63,15 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do worker.perform(12345) end end + + def stub_aggregation_schedule_statistics + # Namespace::Aggregations are deleted by + # Namespace::AggregationSchedule::schedule_root_storage_statistics, + # which is executed async. Stubing the service so instances are not deleted + # while still running the specs. + expect_next_instance_of(Namespace::AggregationSchedule) do |aggregation_schedule| + expect(aggregation_schedule) + .to receive(:schedule_root_storage_statistics) + end + end end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 51afb076da1..edc55920b8e 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -36,6 +36,11 @@ describe ProjectCacheWorker do end context 'with an existing project' do + before do + lease_key = "namespace:namespaces_root_statistics:#{project.namespace_id}" + stub_exclusive_lease_taken(lease_key, timeout: Namespace::AggregationSchedule::DEFAULT_LEASE_TIMEOUT) + end + it 'refreshes the method caches' do expect_any_instance_of(Repository).to receive(:refresh_method_caches) .with(%i(readme)) @@ -81,6 +86,10 @@ describe ProjectCacheWorker do expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in) + expect(Namespaces::ScheduleAggregationWorker) + .not_to receive(:perform_async) + .with(project.namespace_id) + worker.update_statistics(project, statistics) end end @@ -98,6 +107,11 @@ describe ProjectCacheWorker do .with(lease_timeout, project.id, statistics) .and_call_original + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async) + .with(project.namespace_id) + .twice + worker.update_statistics(project, statistics) end end |