diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-02 15:09:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-02 15:09:03 +0300 |
commit | 6092dcc437ef3e9300cc32cb7c6daea9448cba40 (patch) | |
tree | 7a93e011871915b658537ef4787b87633ada5178 /spec | |
parent | 251d3d2b234a4b449edefec4ed8dcf9bc2f8be37 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
88 files changed, 808 insertions, 828 deletions
diff --git a/spec/channels/application_cable/connection_spec.rb b/spec/channels/application_cable/connection_spec.rb index e5f7ea1103c..7d60548f780 100644 --- a/spec/channels/application_cable/connection_spec.rb +++ b/spec/channels/application_cable/connection_spec.rb @@ -5,27 +5,39 @@ require 'spec_helper' RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') } - before do - Gitlab::Redis::SharedState.with do |redis| - redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) + context 'when session cookie is set' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) + end + + cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id end - cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id - end + context 'when user is logged in' do + let(:user) { create(:user) } + let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } } + + it 'sets current_user' do + connect + + expect(connection.current_user).to eq(user) + end - context 'when user is logged in' do - let(:user) { create(:user) } - let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } } + context 'with a stale password' do + let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] } + let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } } - it 'sets current_user' do - connect + it 'sets current_user to nil' do + connect - expect(connection.current_user).to eq(user) + expect(connection.current_user).to be_nil + end + end end - context 'with a stale password' do - let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] } - let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } } + context 'when user is not logged in' do + let(:session_hash) { {} } it 'sets current_user to nil' do connect @@ -35,10 +47,18 @@ RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do end end - context 'when user is not logged in' do - let(:session_hash) { {} } + context 'when session cookie is not set' do + it 'sets current_user to nil' do + connect + + expect(connection.current_user).to be_nil + end + end + context 'when session cookie is an empty string' do it 'sets current_user to nil' do + cookies[Gitlab::Application.config.session_options[:key]] = '' + connect expect(connection.current_user).to be_nil diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index ec7d18caa16..7bba44fc524 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -87,6 +87,38 @@ RSpec.describe Admin::ApplicationSettingsController do sign_in(admin) end + context 'require_admin_approval_after_user_signup setting' do + subject do + put :update, params: { application_setting: { require_admin_approval_after_user_signup: true } } + end + + context 'when feature is enabled' do + before do + stub_feature_flags(admin_approval_for_new_user_signups: true) + end + + it 'updates the require_admin_approval_after_user_signup setting' do + subject + + expect(response).to redirect_to(general_admin_application_settings_path) + expect(ApplicationSetting.current.require_admin_approval_after_user_signup).to eq(true) + end + end + + context 'when feature is disabled' do + before do + stub_feature_flags(admin_approval_for_new_user_signups: false) + end + + it 'does not update the require_admin_approval_after_user_signup setting' do + subject + + expect(response).to redirect_to(general_admin_application_settings_path) + expect(ApplicationSetting.current.require_admin_approval_after_user_signup).not_to eq(true) + end + end + end + it 'updates the password_authentication_enabled_for_git setting' do put :update, params: { application_setting: { password_authentication_enabled_for_git: "0" } } diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb index 35982e57034..5fa7a7f278d 100644 --- a/spec/controllers/admin/sessions_controller_spec.rb +++ b/spec/controllers/admin/sessions_controller_spec.rb @@ -109,7 +109,7 @@ RSpec.describe Admin::SessionsController, :do_not_mock_admin_mode do # triggering the auth form will request admin mode get :new - Timecop.freeze(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do + travel_to(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do post :create, params: { user: { password: user.password } } expect(response).to redirect_to(new_admin_session_path) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 188a4cb04af..94e110325f9 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -416,13 +416,13 @@ RSpec.describe ApplicationController do end it 'returns false if the grace period has expired' do - Timecop.freeze(3.hours.from_now) do + travel_to(3.hours.from_now) do expect(subject).to be_falsey end end it 'returns true if the grace period is still active' do - Timecop.freeze(1.hour.from_now) do + travel_to(1.hour.from_now) do expect(subject).to be_truthy end end diff --git a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb b/spec/controllers/concerns/controller_with_feature_category/config_spec.rb deleted file mode 100644 index 9b8ffd2baab..00000000000 --- a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require "fast_spec_helper" -require "rspec-parameterized" -require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config" - -RSpec.describe ControllerWithFeatureCategory::Config do - describe "#matches?" do - using RSpec::Parameterized::TableSyntax - - where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do - nil | nil | nil | nil | "action" | true - [:included] | nil | nil | nil | "action" | false - [:included] | nil | nil | nil | "included" | true - nil | [:excluded] | nil | nil | "excluded" | false - nil | nil | true | nil | "action" | true - [:included] | nil | true | nil | "action" | false - [:included] | nil | true | nil | "included" | true - nil | [:excluded] | true | nil | "excluded" | false - nil | nil | false | nil | "action" | false - [:included] | nil | false | nil | "action" | false - [:included] | nil | false | nil | "included" | false - nil | [:excluded] | false | nil | "excluded" | false - nil | nil | nil | true | "action" | false - [:included] | nil | nil | true | "action" | false - [:included] | nil | nil | true | "included" | false - nil | [:excluded] | nil | true | "excluded" | false - nil | nil | nil | false | "action" | true - [:included] | nil | nil | false | "action" | false - [:included] | nil | nil | false | "included" | true - nil | [:excluded] | nil | false | "excluded" | false - nil | nil | true | false | "action" | true - [:included] | nil | true | false | "action" | false - [:included] | nil | true | false | "included" | true - nil | [:excluded] | true | false | "excluded" | false - nil | nil | false | true | "action" | false - [:included] | nil | false | true | "action" | false - [:included] | nil | false | true | "included" | false - nil | [:excluded] | false | true | "excluded" | false - end - - with_them do - let(:config) do - if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc } - unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc } - - described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc) - end - - specify { expect(config.matches?(test_action)).to be(expected) } - end - end -end diff --git a/spec/controllers/concerns/controller_with_feature_category_spec.rb b/spec/controllers/concerns/controller_with_feature_category_spec.rb index e603a7d14c4..55e84755f5c 100644 --- a/spec/controllers/concerns/controller_with_feature_category_spec.rb +++ b/spec/controllers/concerns/controller_with_feature_category_spec.rb @@ -2,7 +2,6 @@ require 'fast_spec_helper' require_relative "../../../app/controllers/concerns/controller_with_feature_category" -require_relative "../../../app/controllers/concerns/controller_with_feature_category/config" RSpec.describe ControllerWithFeatureCategory do describe ".feature_category_for_action" do @@ -14,17 +13,15 @@ RSpec.describe ControllerWithFeatureCategory do let(:controller) do Class.new(base_controller) do - feature_category :baz - feature_category :foo, except: %w(update edit) - feature_category :bar, only: %w(index show) - feature_category :quux, only: %w(destroy) - feature_category :quuz, only: %w(destroy) + feature_category :foo, %w(update edit) + feature_category :bar, %w(index show) + feature_category :quux, %w(destroy) end end let(:subclass) do Class.new(controller) do - feature_category :qux, only: %w(index) + feature_category :baz, %w(subclass_index) end end @@ -33,34 +30,31 @@ RSpec.describe ControllerWithFeatureCategory do end it "returns the expected category", :aggregate_failures do - expect(controller.feature_category_for_action("update")).to eq(:baz) - expect(controller.feature_category_for_action("hello")).to eq(:foo) + expect(controller.feature_category_for_action("update")).to eq(:foo) expect(controller.feature_category_for_action("index")).to eq(:bar) + expect(controller.feature_category_for_action("destroy")).to eq(:quux) end - it "returns the closest match for categories defined in subclasses" do - expect(subclass.feature_category_for_action("index")).to eq(:qux) - expect(subclass.feature_category_for_action("show")).to eq(:bar) + it "returns the expected category for categories defined in subclasses" do + expect(subclass.feature_category_for_action("subclass_index")).to eq(:baz) end - it "returns the last defined feature category when multiple match" do - expect(controller.feature_category_for_action("destroy")).to eq(:quuz) - end - - it "raises an error when using including and excluding the same action" do + it "raises an error when defining for the controller and for individual actions" do expect do Class.new(base_controller) do - feature_category :hello, only: [:world], except: [:world] + feature_category :hello + feature_category :goodbye, [:world] end - end.to raise_error(%r(cannot configure both `only` and `except`)) + end.to raise_error(ArgumentError, "hello is defined for all actions, but other categories are set") end - it "raises an error when using unknown arguments" do + it "raises an error when multiple calls define the same action" do expect do Class.new(base_controller) do - feature_category :hello, hello: :world + feature_category :hello, [:world] + feature_category :goodbye, ["world"] end - end.to raise_error(%r(unknown arguments)) + end.to raise_error(ArgumentError, "Actions have multiple feature categories: world") end end end diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb index 4785ee9ed8f..d333c98ccf7 100644 --- a/spec/controllers/every_controller_spec.rb +++ b/spec/controllers/every_controller_spec.rb @@ -17,20 +17,27 @@ RSpec.describe "Every controller" do .compact .select { |route| route[:controller].present? && route[:action].present? } .map { |route| [constantize_controller(route[:controller]), route[:action]] } - .reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) } + .select { |(controller, action)| controller&.include?(ControllerWithFeatureCategory) } + .reject { |(controller, action)| controller == Devise::UnlocksController } end let_it_be(:routes_without_category) do controller_actions.map do |controller, action| - "#{controller}##{action}" unless controller.feature_category_for_action(action) + next if controller.feature_category_for_action(action) + next unless controller.to_s.start_with?('B', 'C', 'D', 'E', 'F', 'Projects::MergeRequestsController') + + "#{controller}##{action}" end.compact end it "has feature categories" do - pending("We'll work on defining categories for all controllers: "\ - "https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463") + routes_without_category.map { |x| x.split('#') }.group_by(&:first).each do |controller, actions| + puts controller + puts actions.map { |x| ":#{x.last}" }.sort.join(', ') + puts '' + end - expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category" + expect(routes_without_category).to be_empty, "#{routes_without_category} did not have a category" end it "completed controllers don't get new routes without categories" do @@ -74,9 +81,9 @@ RSpec.describe "Every controller" do end def actions_defined_in_feature_category_config(controller) - feature_category_configs = controller.send(:class_attributes)[:feature_category_config] - feature_category_configs.map do |config| - Array(config.send(:only)) + Array(config.send(:except)) - end.flatten.uniq.map(&:to_s) + controller.send(:class_attributes)[:feature_category_config] + .values + .flatten + .map(&:to_s) end end diff --git a/spec/controllers/projects/releases/evidences_controller_spec.rb b/spec/controllers/projects/releases/evidences_controller_spec.rb index d5a9665d6a5..0ec4cdf2a31 100644 --- a/spec/controllers/projects/releases/evidences_controller_spec.rb +++ b/spec/controllers/projects/releases/evidences_controller_spec.rb @@ -113,18 +113,6 @@ RSpec.describe Projects::Releases::EvidencesController do it_behaves_like 'does not show the issue in evidence' - context 'when the issue is confidential' do - let(:issue) { create(:issue, :confidential, project: project) } - - it_behaves_like 'does not show the issue in evidence' - end - - context 'when the user is the author of the confidential issue' do - let(:issue) { create(:issue, :confidential, project: project, author: user) } - - it_behaves_like 'does not show the issue in evidence' - end - context 'when project is private' do let(:project) { create(:project, :repository, :private) } @@ -143,32 +131,16 @@ RSpec.describe Projects::Releases::EvidencesController do it_behaves_like 'does not show the issue in evidence' - context 'when the issue is confidential' do - let(:issue) { create(:issue, :confidential, project: project) } - - it_behaves_like 'does not show the issue in evidence' - end - - context 'when the user is the author of the confidential issue' do - let(:issue) { create(:issue, :confidential, project: project, author: user) } - - it_behaves_like 'does not show the issue in evidence' - end - context 'when project is private' do let(:project) { create(:project, :repository, :private) } - it 'returns evidence ' do - subject - - expect(json_response).to eq(evidence.summary) - end + it_behaves_like 'does not show the issue in evidence' end context 'when project restricts the visibility of issues to project members only' do let(:project) { create(:project, :repository, :issues_private) } - it_behaves_like 'evidence not found' + it_behaves_like 'does not show the issue in evidence' end end diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index d213d003bed..57760088183 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -131,4 +131,25 @@ RSpec.describe Projects::TagsController do end end end + + describe 'DELETE #destroy' do + let(:tag) { project.repository.add_tag(user, 'fake-tag', 'master') } + let(:request) do + delete(:destroy, params: { id: tag.name, namespace_id: project.namespace.to_param, project_id: project }) + end + + before do + project.add_developer(user) + sign_in(user) + end + + it 'deletes tag' do + request + + expect(response).to be_successful + expect(response.body).to include("Tag was removed") + + expect(project.repository.find_tag(tag.name)).not_to be_present + end + end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 688539f2a03..47d234df22b 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -78,6 +78,9 @@ RSpec.describe SessionsController do end context 'when using standard authentications' do + let(:user) { create(:user) } + let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) } + context 'invalid password' do it 'does not authenticate user' do post(:create, params: { user: { login: 'invalid', password: 'invalid' } }) @@ -87,6 +90,26 @@ RSpec.describe SessionsController do end end + context 'a blocked user' do + it 'does not authenticate the user' do + user.block! + post_action + + expect(@request.env['warden']).not_to be_authenticated + expect(flash[:alert]).to include('Your account has been blocked') + end + end + + context 'an internal user' do + it 'does not authenticate the user' do + user.ghost! + post_action + + expect(@request.env['warden']).not_to be_authenticated + expect(flash[:alert]).to include('Your account does not have the required permission to login') + end + end + context 'when using valid password', :clean_gitlab_redis_shared_state do let(:user) { create(:user) } let(:user_params) { { login: user.username, password: user.password } } diff --git a/spec/factories/issue_email_participants.rb b/spec/factories/issue_email_participants.rb new file mode 100644 index 00000000000..730e224b01e --- /dev/null +++ b/spec/factories/issue_email_participants.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :issue_email_participant do + issue + email { generate(:email) } + end +end diff --git a/spec/factories/project_tracing_settings.rb b/spec/factories/project_tracing_settings.rb new file mode 100644 index 00000000000..05c1529c18e --- /dev/null +++ b/spec/factories/project_tracing_settings.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :project_tracing_setting do + project + external_url { 'https://example.com' } + end +end diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb index a2eaee54ee9..2f7c6fde3e4 100644 --- a/spec/factories/usage_data.rb +++ b/spec/factories/usage_data.rb @@ -50,6 +50,9 @@ FactoryBot.define do create(:protected_branch, project: projects[0]) create(:protected_branch, name: 'main', project: projects[0]) + # Tracing + create(:project_tracing_setting, project: projects[0]) + # Incident Labeled Issues incident_label = create(:label, :incident, project: projects[0]) create(:labeled_issue, project: projects[0], labels: [incident_label]) diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 38f0b813183..e1df1c69351 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -130,6 +130,38 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n expect(user_internal_regex['placeholder']).to eq 'Regex pattern' end + context 'Change Sign-up restrictions' do + context 'Require Admin approval for new signup setting' do + context 'when feature is enabled' do + before do + stub_feature_flags(admin_approval_for_new_user_signups: true) + end + + it 'changes the setting' do + page.within('.as-signup') do + check 'Require admin approval for new sign-ups' + click_button 'Save changes' + end + + expect(current_settings.require_admin_approval_after_user_signup).to be_truthy + expect(page).to have_content "Application settings saved successfully" + end + end + + context 'when feature is disabled' do + before do + stub_feature_flags(admin_approval_for_new_user_signups: false) + end + + it 'does not show the the setting' do + page.within('.as-signup') do + expect(page).not_to have_selector('.application_setting_require_admin_approval_after_user_signup') + end + end + end + end + end + it 'Change Sign-in restrictions' do page.within('.as-signin') do fill_in 'Home page URL', with: 'https://about.gitlab.com/' diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 346f305f0d0..5f58fa420fb 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -180,7 +180,7 @@ RSpec.describe 'Contributions Calendar', :js do before do push_code_contribution - Timecop.freeze(Date.yesterday) do + travel_to(Date.yesterday) do Issues::CreateService.new(contributed_project, user, issue_params).execute end end diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index 40f6482c948..c8fc23bebf9 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -41,7 +41,6 @@ RSpec.describe 'Merge request > Batch comments', :js do write_comment page.within('.review-bar-content') do - click_button 'Finish review' click_button 'Submit review' end @@ -64,18 +63,6 @@ RSpec.describe 'Merge request > Batch comments', :js do expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong') end - it 'discards review' do - write_comment - - click_button 'Discard review' - - click_button 'Delete all pending comments' - - wait_for_requests - - expect(page).not_to have_selector('.draft-note-component') - end - it 'deletes draft note' do write_comment @@ -149,7 +136,6 @@ RSpec.describe 'Merge request > Batch comments', :js do write_reply_to_discussion(resolve: true) page.within('.review-bar-content') do - click_button 'Finish review' click_button 'Submit review' end @@ -192,7 +178,6 @@ RSpec.describe 'Merge request > Batch comments', :js do write_reply_to_discussion(button_text: 'Start a review', unresolve: true) page.within('.review-bar-content') do - click_button 'Finish review' click_button 'Submit review' end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 0e2444c5434..4224fdbc1fc 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -21,11 +21,11 @@ RSpec.describe 'Branches' do before do # Add 4 stale branches (1..4).reverse_each do |i| - Timecop.freeze((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") } + travel_to((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") } end # Add 6 active branches (1..6).each do |i| - Timecop.freeze((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") } + travel_to((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") } end end diff --git a/spec/features/tags/developer_deletes_tag_spec.rb b/spec/features/tags/developer_deletes_tag_spec.rb index de9296bc08e..7c4c6f54685 100644 --- a/spec/features/tags/developer_deletes_tag_spec.rb +++ b/spec/features/tags/developer_deletes_tag_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Developer deletes tag' do +RSpec.describe 'Developer deletes tag', :js do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, :repository, namespace: group) } @@ -13,11 +13,12 @@ RSpec.describe 'Developer deletes tag' do visit project_tags_path(project) end - context 'from the tags list page', :js do + context 'from the tags list page' do it 'deletes the tag' do expect(page).to have_content 'v1.1.0' - delete_tag 'v1.1.0' + container = page.find('.content .flex-row', text: 'v1.1.0') + delete_tag container expect(page).not_to have_content 'v1.1.0' end @@ -29,15 +30,15 @@ RSpec.describe 'Developer deletes tag' do expect(current_path).to eq( project_tag_path(project, 'v1.0.0')) - click_on 'Delete tag' + container = page.find('.nav-controls') + delete_tag container - expect(current_path).to eq( - project_tags_path(project)) + expect(current_path).to eq("#{project_tags_path(project)}/") expect(page).not_to have_content 'v1.0.0' end end - context 'when pre-receive hook fails', :js do + context 'when pre-receive hook fails' do before do allow_next_instance_of(Gitlab::GitalyClient::OperationService) do |instance| allow(instance).to receive(:rm_tag) @@ -46,15 +47,17 @@ RSpec.describe 'Developer deletes tag' do end it 'shows the error message' do - delete_tag 'v1.1.0' + container = page.find('.content .flex-row', text: 'v1.1.0') + delete_tag container expect(page).to have_content('Do not delete tags') end end - def delete_tag(tag) - page.within('.content') do - accept_confirm { find("li > .row-fixed-content.controls a.btn-remove[href='/#{project.full_path}/-/tags/#{tag}']").click } - end + def delete_tag(container) + container.find('.js-remove-tag').click + + page.within('.modal') { click_button('Delete tag') } + wait_for_requests end end diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js index 2b63ece28ba..8ddad3dacfe 100644 --- a/spec/frontend/batch_comments/components/preview_item_spec.js +++ b/spec/frontend/batch_comments/components/preview_item_spec.js @@ -43,22 +43,6 @@ describe('Batch comments draft preview item component', () => { ); }); - it('adds is last class', () => { - createComponent(true); - - expect(vm.$el.classList).toContain('is-last'); - }); - - it('scrolls to draft on click', () => { - createComponent(); - - jest.spyOn(vm.$store, 'dispatch').mockImplementation(); - - vm.$el.click(); - - expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/scrollToDraft', vm.draft); - }); - describe('for file', () => { it('renders file path', () => { createComponent(false, { file_path: 'index.js', file_hash: 'abc', position: {} }); diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js index 4362f62c7f8..4032713150c 100644 --- a/spec/frontend/batch_comments/components/publish_button_spec.js +++ b/spec/frontend/batch_comments/components/publish_button_spec.js @@ -29,17 +29,6 @@ describe('Batch comments publish button component', () => { expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined); }); - it('dispatches toggleReviewDropdown when shouldPublish is false on click', () => { - vm.shouldPublish = false; - - vm.$el.click(); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - 'batchComments/toggleReviewDropdown', - undefined, - ); - }); - it('sets loading when isPublishing is true', done => { vm.$store.state.batchComments.isPublishing = true; diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js index fb3c532174d..f235867f002 100644 --- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js @@ -1,96 +1,39 @@ -import Vue from 'vue'; -import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue'; import { createStore } from '~/mr_notes/stores'; import '~/behaviors/markdown/render_gfm'; import { createDraft } from '../mock_data'; +const localVue = createLocalVue(); +localVue.use(Vuex); + describe('Batch comments publish dropdown component', () => { - let vm; - let Component; + let wrapper; - function createComponent(extendStore = () => {}) { + function createComponent() { const store = createStore(); store.state.batchComments.drafts.push(createDraft(), { ...createDraft(), id: 2 }); - extendStore(store); - - vm = mountComponentWithStore(Component, { store }); + wrapper = shallowMount(PreviewDropdown, { + store, + }); } - beforeAll(() => { - Component = Vue.extend(PreviewDropdown); - }); - afterEach(() => { - vm.$destroy(); - }); - - it('toggles dropdown when clicking button', done => { - createComponent(); - - jest.spyOn(vm.$store, 'dispatch'); - - vm.$el.querySelector('.review-preview-dropdown-toggle').click(); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - 'batchComments/toggleReviewDropdown', - expect.anything(), - ); - - setImmediate(() => { - expect(vm.$el.classList).toContain('show'); - - done(); - }); - }); - - it('toggles dropdown when clicking body', () => { - createComponent(); - - vm.$store.state.batchComments.showPreviewDropdown = true; - - jest.spyOn(vm.$store, 'dispatch').mockImplementation(); - - document.body.click(); - - expect(vm.$store.dispatch).toHaveBeenCalledWith( - 'batchComments/toggleReviewDropdown', - undefined, - ); + wrapper.destroy(); }); it('renders list of drafts', () => { - createComponent(store => { - Object.assign(store.state.notes, { - isNotesFetched: true, - }); - }); - - expect(vm.$el.querySelectorAll('.dropdown-content li').length).toBe(2); - }); - - it('adds is-last class to last item', () => { - createComponent(store => { - Object.assign(store.state.notes, { - isNotesFetched: true, - }); - }); - - expect(vm.$el.querySelectorAll('.dropdown-content li')[1].querySelector('.is-last')).not.toBe( - null, - ); - }); - - it('renders draft count in dropdown title', () => { createComponent(); - expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('2 pending comments'); + expect(wrapper.findAll(GlDropdownItem).length).toBe(2); }); - it('renders publish button in footer', () => { + it('renders draft count in dropdown title', () => { createComponent(); - expect(vm.$el.querySelector('.dropdown-footer .js-publish-draft-button')).not.toBe(null); + expect(wrapper.find(GlDropdown).props('headerText')).toEqual('2 pending comments'); }); }); diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js index a6942115649..e66f36aa3a2 100644 --- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js +++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js @@ -199,42 +199,6 @@ describe('Batch comments store actions', () => { }); }); - describe('discardReview', () => { - it('commits mutations', done => { - const getters = { - getNotesData: { draftsDiscardPath: TEST_HOST }, - }; - const commit = jest.fn(); - mock.onAny().reply(200); - - actions - .discardReview({ getters, commit }) - .then(() => { - expect(commit.mock.calls[0]).toEqual(['REQUEST_DISCARD_REVIEW']); - expect(commit.mock.calls[1]).toEqual(['RECEIVE_DISCARD_REVIEW_SUCCESS']); - }) - .then(done) - .catch(done.fail); - }); - - it('commits error mutations', done => { - const getters = { - getNotesData: { draftsDiscardPath: TEST_HOST }, - }; - const commit = jest.fn(); - mock.onAny().reply(500); - - actions - .discardReview({ getters, commit }) - .then(() => { - expect(commit.mock.calls[0]).toEqual(['REQUEST_DISCARD_REVIEW']); - expect(commit.mock.calls[1]).toEqual(['RECEIVE_DISCARD_REVIEW_ERROR']); - }) - .then(done) - .catch(done.fail); - }); - }); - describe('updateDraft', () => { let getters; @@ -284,56 +248,6 @@ describe('Batch comments store actions', () => { }); }); - describe('toggleReviewDropdown', () => { - it('dispatches openReviewDropdown', done => { - testAction( - actions.toggleReviewDropdown, - null, - { showPreviewDropdown: false }, - [], - [{ type: 'openReviewDropdown' }], - done, - ); - }); - - it('dispatches closeReviewDropdown when showPreviewDropdown is true', done => { - testAction( - actions.toggleReviewDropdown, - null, - { showPreviewDropdown: true }, - [], - [{ type: 'closeReviewDropdown' }], - done, - ); - }); - }); - - describe('openReviewDropdown', () => { - it('commits OPEN_REVIEW_DROPDOWN', done => { - testAction( - actions.openReviewDropdown, - null, - null, - [{ type: 'OPEN_REVIEW_DROPDOWN' }], - [], - done, - ); - }); - }); - - describe('closeReviewDropdown', () => { - it('commits CLOSE_REVIEW_DROPDOWN', done => { - testAction( - actions.closeReviewDropdown, - null, - null, - [{ type: 'CLOSE_REVIEW_DROPDOWN' }], - [], - done, - ); - }); - }); - describe('expandAllDiscussions', () => { it('dispatches expandDiscussion for all drafts', done => { const state = { @@ -383,9 +297,7 @@ describe('Batch comments store actions', () => { actions.scrollToDraft({ dispatch, rootGetters }, draft); - expect(dispatch.mock.calls[0]).toEqual(['closeReviewDropdown']); - - expect(dispatch.mock.calls[1]).toEqual([ + expect(dispatch.mock.calls[0]).toEqual([ 'expandDiscussion', { discussionId: '1' }, { root: true }, diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js index a86726269ef..1406f66fd10 100644 --- a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js +++ b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js @@ -89,42 +89,6 @@ describe('Batch comments mutations', () => { }); }); - describe(types.REQUEST_DISCARD_REVIEW, () => { - it('sets isDiscarding to true', () => { - mutations[types.REQUEST_DISCARD_REVIEW](state); - - expect(state.isDiscarding).toBe(true); - }); - }); - - describe(types.RECEIVE_DISCARD_REVIEW_SUCCESS, () => { - it('emptys drafts array', () => { - state.drafts.push('test'); - - mutations[types.RECEIVE_DISCARD_REVIEW_SUCCESS](state); - - expect(state.drafts).toEqual([]); - }); - - it('sets isDiscarding to false', () => { - state.isDiscarding = true; - - mutations[types.RECEIVE_DISCARD_REVIEW_SUCCESS](state); - - expect(state.isDiscarding).toBe(false); - }); - }); - - describe(types.RECEIVE_DISCARD_REVIEW_ERROR, () => { - it('updates isDiscarding to false', () => { - state.isDiscarding = true; - - mutations[types.RECEIVE_DISCARD_REVIEW_ERROR](state); - - expect(state.isDiscarding).toBe(false); - }); - }); - describe(types.RECEIVE_DRAFT_UPDATE_SUCCESS, () => { it('updates draft in store', () => { state.drafts.push({ id: 1 }); @@ -140,20 +104,4 @@ describe('Batch comments mutations', () => { ]); }); }); - - describe(types.OPEN_REVIEW_DROPDOWN, () => { - it('sets showPreviewDropdown to true', () => { - mutations[types.OPEN_REVIEW_DROPDOWN](state); - - expect(state.showPreviewDropdown).toBe(true); - }); - }); - - describe(types.CLOSE_REVIEW_DROPDOWN, () => { - it('sets showPreviewDropdown to false', () => { - mutations[types.CLOSE_REVIEW_DROPDOWN](state); - - expect(state.showPreviewDropdown).toBe(false); - }); - }); }); diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js index f5df8c180d5..d4aa29eaadd 100644 --- a/spec/frontend/groups/components/item_actions_spec.js +++ b/spec/frontend/groups/components/item_actions_spec.js @@ -1,84 +1,87 @@ -import Vue from 'vue'; - -import mountComponent from 'helpers/vue_mount_component_helper'; -import itemActionsComponent from '~/groups/components/item_actions.vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import ItemActions from '~/groups/components/item_actions.vue'; import eventHub from '~/groups/event_hub'; import { mockParentGroupItem, mockChildren } from '../mock_data'; -const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { - const Component = Vue.extend(itemActionsComponent); +describe('ItemActions', () => { + let wrapper; + const parentGroup = mockChildren[0]; - return mountComponent(Component, { - group, + const defaultProps = { + group: mockParentGroupItem, parentGroup, - }); -}; - -describe('ItemActionsComponent', () => { - let vm; + }; - beforeEach(() => { - vm = createComponent(); - }); + const createComponent = (props = {}) => { + wrapper = shallowMount(ItemActions, { + propsData: { ...defaultProps, ...props }, + }); + }; afterEach(() => { - vm.$destroy(); + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } }); - describe('methods', () => { - describe('onLeaveGroup', () => { - it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - vm.onLeaveGroup(); - - expect(eventHub.$emit).toHaveBeenCalledWith( - 'showLeaveGroupModal', - vm.group, - vm.parentGroup, - ); - }); - }); - }); + const findEditGroupBtn = () => wrapper.find('[data-testid="edit-group-btn"]'); + const findEditGroupIcon = () => findEditGroupBtn().find(GlIcon); + const findLeaveGroupBtn = () => wrapper.find('[data-testid="leave-group-btn"]'); + const findLeaveGroupIcon = () => findLeaveGroupBtn().find(GlIcon); describe('template', () => { - it('should render component template correctly', () => { - expect(vm.$el.classList.contains('controls')).toBeTruthy(); - }); + it('renders component template correctly', () => { + createComponent(); - it('should render Edit Group button with correct attribute values', () => { - const group = { ...mockParentGroupItem }; - group.canEdit = true; - const newVm = createComponent(group); + expect(wrapper.classes()).toContain('controls'); + }); - const editBtn = newVm.$el.querySelector('a.edit-group'); + it('renders "Edit group" button with correct attribute values', () => { + const group = { + ...mockParentGroupItem, + canEdit: true, + }; + + createComponent({ group }); + + expect(findEditGroupBtn().exists()).toBe(true); + expect(findEditGroupBtn().classes()).toContain('no-expand'); + expect(findEditGroupBtn().attributes('href')).toBe(group.editPath); + expect(findEditGroupBtn().attributes('aria-label')).toBe('Edit group'); + expect(findEditGroupBtn().attributes('data-original-title')).toBe('Edit group'); + expect(findEditGroupIcon().exists()).toBe(true); + expect(findEditGroupIcon().props('name')).toBe('settings'); + }); - expect(editBtn).toBeDefined(); - expect(editBtn.classList.contains('no-expand')).toBeTruthy(); - expect(editBtn.getAttribute('href')).toBe(group.editPath); - expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); - expect(editBtn.dataset.originalTitle).toBe('Edit group'); - expect(editBtn.querySelectorAll('svg').length).not.toBe(0); - expect(editBtn.querySelector('svg').getAttribute('data-testid')).toBe('settings-icon'); + describe('`canLeave` is true', () => { + const group = { + ...mockParentGroupItem, + canLeave: true, + }; - newVm.$destroy(); - }); + beforeEach(() => { + createComponent({ group }); + }); - it('should render Leave Group button with correct attribute values', () => { - const group = { ...mockParentGroupItem }; - group.canLeave = true; - const newVm = createComponent(group); + it('renders "Leave this group" button with correct attribute values', () => { + expect(findLeaveGroupBtn().exists()).toBe(true); + expect(findLeaveGroupBtn().classes()).toContain('no-expand'); + expect(findLeaveGroupBtn().attributes('href')).toBe(group.leavePath); + expect(findLeaveGroupBtn().attributes('aria-label')).toBe('Leave this group'); + expect(findLeaveGroupBtn().attributes('data-original-title')).toBe('Leave this group'); + expect(findLeaveGroupIcon().exists()).toBe(true); + expect(findLeaveGroupIcon().props('name')).toBe('leave'); + }); - const leaveBtn = newVm.$el.querySelector('a.leave-group'); + it('emits event on "Leave this group" button click', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - expect(leaveBtn).toBeDefined(); - expect(leaveBtn.classList.contains('no-expand')).toBeTruthy(); - expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); - expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); - expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); - expect(leaveBtn.querySelectorAll('svg').length).not.toBe(0); - expect(leaveBtn.querySelector('svg').getAttribute('data-testid')).toBe('leave-icon'); + findLeaveGroupBtn().trigger('click'); - newVm.$destroy(); + expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', group, parentGroup); + }); }); }); }); diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js index 4ff7482414c..b2915607a06 100644 --- a/spec/frontend/groups/components/item_caret_spec.js +++ b/spec/frontend/groups/components/item_caret_spec.js @@ -1,38 +1,48 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import ItemCaret from '~/groups/components/item_caret.vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import itemCaretComponent from '~/groups/components/item_caret.vue'; +describe('ItemCaret', () => { + let wrapper; -const createComponent = (isGroupOpen = false) => { - const Component = Vue.extend(itemCaretComponent); + const defaultProps = { + isGroupOpen: false, + }; - return mountComponent(Component, { - isGroupOpen, - }); -}; - -describe('ItemCaretComponent', () => { - let vm; + const createComponent = (props = {}) => { + wrapper = shallowMount(ItemCaret, { + propsData: { ...defaultProps, ...props }, + }); + }; afterEach(() => { - vm.$destroy(); + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } }); + const findAllGlIcons = () => wrapper.findAll(GlIcon); + const findGlIcon = () => wrapper.find(GlIcon); + describe('template', () => { - it('should render component template correctly', () => { - vm = createComponent(); - expect(vm.$el.classList.contains('folder-caret')).toBeTruthy(); - expect(vm.$el.querySelectorAll('svg').length).toBe(1); - }); + it('renders component template correctly', () => { + createComponent(); - it('should render caret down icon if `isGroupOpen` prop is `true`', () => { - vm = createComponent(true); - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('angle-down-icon'); + expect(wrapper.classes()).toContain('folder-caret'); + expect(findAllGlIcons()).toHaveLength(1); }); - it('should render caret right icon if `isGroupOpen` prop is `false`', () => { - vm = createComponent(); - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('angle-right-icon'); + it.each` + isGroupOpen | icon + ${true} | ${'angle-down'} + ${false} | ${'angle-right'} + `('renders "$icon" icon when `isGroupOpen` is $isGroupOpen', ({ isGroupOpen, icon }) => { + createComponent({ + isGroupOpen, + }); + + expect(findGlIcon().props('name')).toBe(icon); }); }); }); diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js index 771643609ec..d8c88a608ac 100644 --- a/spec/frontend/groups/components/item_stats_spec.js +++ b/spec/frontend/groups/components/item_stats_spec.js @@ -1,119 +1,50 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import ItemStats from '~/groups/components/item_stats.vue'; +import ItemStatsValue from '~/groups/components/item_stats_value.vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import itemStatsComponent from '~/groups/components/item_stats.vue'; -import { - mockParentGroupItem, - ITEM_TYPE, - VISIBILITY_TYPE_ICON, - GROUP_VISIBILITY_TYPE, - PROJECT_VISIBILITY_TYPE, -} from '../mock_data'; +import { mockParentGroupItem, ITEM_TYPE } from '../mock_data'; -const createComponent = (item = mockParentGroupItem) => { - const Component = Vue.extend(itemStatsComponent); +describe('ItemStats', () => { + let wrapper; - return mountComponent(Component, { - item, - }); -}; - -describe('ItemStatsComponent', () => { - describe('computed', () => { - describe('visibilityIcon', () => { - it('should return icon class based on `item.visibility` value', () => { - Object.keys(VISIBILITY_TYPE_ICON).forEach(visibility => { - const item = { ...mockParentGroupItem, visibility }; - const vm = createComponent(item); + const defaultProps = { + item: mockParentGroupItem, + }; - expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]); - vm.$destroy(); - }); - }); + const createComponent = (props = {}) => { + wrapper = shallowMount(ItemStats, { + propsData: { ...defaultProps, ...props }, }); + }; - describe('visibilityTooltip', () => { - it('should return tooltip string for Group based on `item.visibility` value', () => { - Object.keys(GROUP_VISIBILITY_TYPE).forEach(visibility => { - const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.GROUP }; - const vm = createComponent(item); - - expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]); - vm.$destroy(); - }); - }); - - it('should return tooltip string for Project based on `item.visibility` value', () => { - Object.keys(PROJECT_VISIBILITY_TYPE).forEach(visibility => { - const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.PROJECT }; - const vm = createComponent(item); - - expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]); - vm.$destroy(); - }); - }); - }); - - describe('isProject', () => { - it('should return boolean value representing whether `item.type` is Project or not', () => { - let item; - let vm; - - item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT }; - vm = createComponent(item); - - expect(vm.isProject).toBeTruthy(); - vm.$destroy(); - - item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP }; - vm = createComponent(item); - - expect(vm.isProject).toBeFalsy(); - vm.$destroy(); - }); - }); - - describe('isGroup', () => { - it('should return boolean value representing whether `item.type` is Group or not', () => { - let item; - let vm; - - item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP }; - vm = createComponent(item); - - expect(vm.isGroup).toBeTruthy(); - vm.$destroy(); - - item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT }; - vm = createComponent(item); - - expect(vm.isGroup).toBeFalsy(); - vm.$destroy(); - }); - }); + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } }); + const findItemStatsValue = () => wrapper.find(ItemStatsValue); + describe('template', () => { it('renders component container element correctly', () => { - const vm = createComponent(); + createComponent(); - expect(vm.$el.classList.contains('stats')).toBeTruthy(); - - vm.$destroy(); + expect(wrapper.classes()).toContain('stats'); }); it('renders start count and last updated information for project item correctly', () => { - const item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT, starCount: 4 }; - const vm = createComponent(item); - - const projectStarIconEl = vm.$el.querySelector('.project-stars'); + const item = { + ...mockParentGroupItem, + type: ITEM_TYPE.PROJECT, + starCount: 4, + }; - expect(projectStarIconEl).not.toBeNull(); - expect(projectStarIconEl.querySelectorAll('svg').length).toBeGreaterThan(0); - expect(projectStarIconEl.querySelectorAll('.stat-value').length).toBeGreaterThan(0); - expect(vm.$el.querySelectorAll('.last-updated').length).toBeGreaterThan(0); + createComponent({ item }); - vm.$destroy(); + expect(findItemStatsValue().exists()).toBe(true); + expect(findItemStatsValue().props('cssClass')).toBe('project-stars'); + expect(wrapper.contains('.last-updated')).toBe(true); }); }); }); diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js index 11246390444..6f018aa79a0 100644 --- a/spec/frontend/groups/components/item_stats_value_spec.js +++ b/spec/frontend/groups/components/item_stats_value_spec.js @@ -1,82 +1,67 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import ItemStatsValue from '~/groups/components/item_stats_value.vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import itemStatsValueComponent from '~/groups/components/item_stats_value.vue'; +describe('ItemStatsValue', () => { + let wrapper; -const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => { - const Component = Vue.extend(itemStatsValueComponent); + const defaultProps = { + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + }; - return mountComponent(Component, { - title, - cssClass, - iconName, - tooltipPlacement, - value, + const createComponent = (props = {}) => { + wrapper = shallowMount(ItemStatsValue, { + propsData: { ...defaultProps, ...props }, + }); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } }); -}; -describe('ItemStatsValueComponent', () => { - describe('computed', () => { - let vm; - const itemConfig = { - title: 'Subgroups', - cssClass: 'number-subgroups', - iconName: 'folder', - tooltipPlacement: 'left', - }; + const findGlIcon = () => wrapper.find(GlIcon); + const findStatValue = () => wrapper.find('[data-testid="itemStatValue"]'); - describe('isValuePresent', () => { - it('returns true if non-empty `value` is present', () => { - vm = createComponent({ ...itemConfig, value: 10 }); + describe('template', () => { + describe('when `value` is not provided', () => { + it('does not render value count', () => { + createComponent(); - expect(vm.isValuePresent).toBeTruthy(); + expect(findStatValue().exists()).toBe(false); }); + }); - it('returns false if empty `value` is present', () => { - vm = createComponent(itemConfig); - - expect(vm.isValuePresent).toBeFalsy(); + describe('when `value` is provided', () => { + beforeEach(() => { + createComponent({ + value: 10, + }); }); - afterEach(() => { - vm.$destroy(); + it('renders component element correctly', () => { + expect(wrapper.classes()).toContain('number-subgroups'); }); - }); - }); - describe('template', () => { - let vm; - beforeEach(() => { - vm = createComponent({ - title: 'Subgroups', - cssClass: 'number-subgroups', - iconName: 'folder', - tooltipPlacement: 'left', - value: 10, + it('renders element tooltip correctly', () => { + expect(wrapper.attributes('data-original-title')).toBe('Subgroups'); + expect(wrapper.attributes('data-placement')).toBe('left'); }); - }); - afterEach(() => { - vm.$destroy(); - }); - - it('renders component element correctly', () => { - expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy(); - expect(vm.$el.querySelectorAll('svg').length).toBeGreaterThan(0); - expect(vm.$el.querySelectorAll('.stat-value').length).toBeGreaterThan(0); - }); - - it('renders element tooltip correctly', () => { - expect(vm.$el.dataset.originalTitle).toBe('Subgroups'); - expect(vm.$el.dataset.placement).toBe('left'); - }); - - it('renders element icon correctly', () => { - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('folder-icon'); - }); + it('renders element icon correctly', () => { + expect(findGlIcon().exists()).toBe(true); + expect(findGlIcon().props('name')).toBe('folder'); + }); - it('renders value count correctly', () => { - expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10'); + it('renders value count correctly', () => { + expect(findStatValue().classes()).toContain('stat-value'); + expect(findStatValue().text()).toBe('10'); + }); }); }); }); diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js index 477c413ddcd..5e7056be218 100644 --- a/spec/frontend/groups/components/item_type_icon_spec.js +++ b/spec/frontend/groups/components/item_type_icon_spec.js @@ -1,53 +1,53 @@ -import Vue from 'vue'; - -import mountComponent from 'helpers/vue_mount_component_helper'; -import itemTypeIconComponent from '~/groups/components/item_type_icon.vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import ItemTypeIcon from '~/groups/components/item_type_icon.vue'; import { ITEM_TYPE } from '../mock_data'; -const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => { - const Component = Vue.extend(itemTypeIconComponent); - - return mountComponent(Component, { - itemType, - isGroupOpen, - }); -}; +describe('ItemTypeIcon', () => { + let wrapper; -describe('ItemTypeIconComponent', () => { - describe('template', () => { - it('should render component template correctly', () => { - const vm = createComponent(); + const defaultProps = { + itemType: ITEM_TYPE.GROUP, + isGroupOpen: false, + }; - expect(vm.$el.classList.contains('item-type-icon')).toBeTruthy(); - vm.$destroy(); + const createComponent = (props = {}) => { + wrapper = shallowMount(ItemTypeIcon, { + propsData: { ...defaultProps, ...props }, }); + }; - it('should render folder open or close icon based `isGroupOpen` prop value', () => { - let vm; - - vm = createComponent(ITEM_TYPE.GROUP, true); + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('folder-open-icon'); - vm.$destroy(); + const findGlIcon = () => wrapper.find(GlIcon); - vm = createComponent(ITEM_TYPE.GROUP); + describe('template', () => { + it('renders component template correctly', () => { + createComponent(); - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('folder-o-icon'); - vm.$destroy(); + expect(wrapper.classes()).toContain('item-type-icon'); }); - it('should render bookmark icon based on `isProject` prop value', () => { - let vm; - - vm = createComponent(ITEM_TYPE.PROJECT); - - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).toBe('bookmark-icon'); - vm.$destroy(); - - vm = createComponent(ITEM_TYPE.GROUP); - - expect(vm.$el.querySelector('svg').getAttribute('data-testid')).not.toBe('bookmark-icon'); - vm.$destroy(); - }); + it.each` + type | isGroupOpen | icon + ${ITEM_TYPE.GROUP} | ${true} | ${'folder-open'} + ${ITEM_TYPE.GROUP} | ${false} | ${'folder-o'} + ${ITEM_TYPE.PROJECT} | ${true} | ${'bookmark'} + ${ITEM_TYPE.PROJECT} | ${false} | ${'bookmark'} + `( + 'shows "$icon" icon when `itemType` is "$type" and `isGroupOpen` is $isGroupOpen', + ({ type, isGroupOpen, icon }) => { + createComponent({ + itemType: type, + isGroupOpen, + }); + expect(findGlIcon().props('name')).toBe(icon); + }, + ); }); }); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 3cc675d2007..2afc1694281 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -701,6 +701,18 @@ describe('URL utility', () => { }); }); + describe('stripFinalUrlSegment', () => { + it.each` + path | expected + ${'http://fake.domain/twitter/typeahead-js/-/tags/v0.11.0'} | ${'http://fake.domain/twitter/typeahead-js/-/tags/'} + ${'http://fake.domain/bar/cool/-/nested/content'} | ${'http://fake.domain/bar/cool/-/nested/'} + ${'http://fake.domain/bar/cool?q="search"'} | ${'http://fake.domain/bar/'} + ${'http://fake.domain/bar/cool#link-to-something'} | ${'http://fake.domain/bar/'} + `('stripFinalUrlSegment $path => $expected', ({ path, expected }) => { + expect(urlUtils.stripFinalUrlSegment(path)).toBe(expected); + }); + }); + describe('escapeFileUrl', () => { it('encodes URL excluding the slashes', () => { expect(urlUtils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md'); diff --git a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js new file mode 100644 index 00000000000..17821d8be31 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js @@ -0,0 +1,71 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlSprintf } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue'; +import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '~/registry/explorer/constants'; + +describe('Partial Cleanup alert', () => { + let wrapper; + + const findAlert = () => wrapper.find(GlAlert); + const findRunLink = () => wrapper.find('[data-testid="run-link"'); + const findHelpLink = () => wrapper.find('[data-testid="help-link"'); + + const mountComponent = () => { + wrapper = shallowMount(component, { + stubs: { GlSprintf }, + propsData: { + runCleanupPoliciesHelpPagePath: 'foo', + cleanupPoliciesHelpPagePath: 'bar', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it(`gl-alert has the correct properties`, () => { + mountComponent(); + + expect(findAlert().props()).toMatchObject({ + title: DELETE_ALERT_TITLE, + variant: 'warning', + }); + }); + + it('has the right text', () => { + mountComponent(); + + expect(wrapper.text()).toMatchInterpolatedText(DELETE_ALERT_LINK_TEXT); + }); + + it('contains run link', () => { + mountComponent(); + + const link = findRunLink(); + expect(link.exists()).toBe(true); + expect(link.attributes()).toMatchObject({ + href: 'foo', + target: '_blank', + }); + }); + + it('contains help link', () => { + mountComponent(); + + const link = findHelpLink(); + expect(link.exists()).toBe(true); + expect(link.attributes()).toMatchObject({ + href: 'bar', + target: '_blank', + }); + }); + + it('GlAlert dismiss event triggers a dismiss event', () => { + mountComponent(); + + findAlert().vm.$emit('dismiss'); + expect(wrapper.emitted('dismiss')).toEqual([[]]); + }); +}); diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index 66e8a4aea0d..86b52c4f06a 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -3,6 +3,7 @@ import { GlPagination } from '@gitlab/ui'; import Tracking from '~/tracking'; import component from '~/registry/explorer/pages/details.vue'; import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue'; +import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue'; import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue'; import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue'; import TagsList from '~/registry/explorer/components/details_page/tags_list.vue'; @@ -30,8 +31,10 @@ describe('Details Page', () => { const findDeleteAlert = () => wrapper.find(DeleteAlert); const findDetailsHeader = () => wrapper.find(DetailsHeader); const findEmptyTagsState = () => wrapper.find(EmptyTagsState); + const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert); - const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' })); + const routeIdGenerator = override => + window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar', ...override })); const tagsArrayToSelectedTags = tags => tags.reduce((acc, c) => { @@ -39,7 +42,7 @@ describe('Details Page', () => { return acc; }, {}); - const mountComponent = options => { + const mountComponent = ({ options, routeParams } = {}) => { wrapper = shallowMount(component, { store, stubs: { @@ -48,7 +51,7 @@ describe('Details Page', () => { mocks: { $route: { params: { - id: routeId, + id: routeIdGenerator(routeParams), }, }, }, @@ -224,7 +227,7 @@ describe('Details Page', () => { findDeleteModal().vm.$emit('confirmDelete'); expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', { tag: store.state.tags[0], - params: routeId, + params: routeIdGenerator(), }); }); }); @@ -239,7 +242,7 @@ describe('Details Page', () => { findDeleteModal().vm.$emit('confirmDelete'); expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', { ids: store.state.tags.map(t => t.name), - params: routeId, + params: routeIdGenerator(), }); }); }); @@ -273,11 +276,57 @@ describe('Details Page', () => { it('has the correct props', () => { store.commit(SET_INITIAL_STATE, { ...config }); mountComponent({ - data: () => ({ - deleteAlertType, - }), + options: { + data: () => ({ + deleteAlertType, + }), + }, }); expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType }); }); }); + + describe('Partial Cleanup Alert', () => { + const config = { + runCleanupPoliciesHelpPagePath: 'foo', + cleanupPoliciesHelpPagePath: 'bar', + }; + + describe('when expiration_policy_started is not null', () => { + const routeParams = { cleanup_policy_started_at: Date.now().toString() }; + + it('exists', () => { + mountComponent({ routeParams }); + + expect(findPartialCleanupAlert().exists()).toBe(true); + }); + + it('has the correct props', () => { + store.commit(SET_INITIAL_STATE, { ...config }); + + mountComponent({ routeParams }); + + expect(findPartialCleanupAlert().props()).toEqual({ ...config }); + }); + + it('dismiss hides the component', async () => { + mountComponent({ routeParams }); + + expect(findPartialCleanupAlert().exists()).toBe(true); + findPartialCleanupAlert().vm.$emit('dismiss'); + + await wrapper.vm.$nextTick(); + + expect(findPartialCleanupAlert().exists()).toBe(false); + }); + }); + + describe('when expiration_policy_started is null', () => { + it('the component is hidden', () => { + mountComponent(); + + expect(findPartialCleanupAlert().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js index 5d92af64de0..8456ca9d125 100644 --- a/spec/frontend/vue_shared/components/confirm_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js @@ -86,6 +86,22 @@ describe('vue_shared/components/confirm_modal', () => { expect(findForm().element.submit).not.toHaveBeenCalled(); }); + describe('with handleSubmit prop', () => { + const handleSubmit = jest.fn(); + beforeEach(() => { + createComponent({ handleSubmit }); + findModal().vm.$emit('primary'); + }); + + it('will call handleSubmit', () => { + expect(handleSubmit).toHaveBeenCalled(); + }); + + it('does not submit the form', () => { + expect(findForm().element.submit).not.toHaveBeenCalled(); + }); + }); + describe('when modal submitted', () => { beforeEach(() => { findModal().vm.$emit('primary'); diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb index c2dbaac7f15..45cc73974d6 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Backup::Files do let(:timestamp) { Time.utc(2017, 3, 22) } around do |example| - Timecop.freeze(timestamp) { example.run } + travel_to(timestamp) { example.run } end describe 'folders with permission' do diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb index 8bdb24ab08c..d29af311ee5 100644 --- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do it_behaves_like 'a metrics embed filter' around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end context 'when grafana is not configured' do diff --git a/spec/lib/gitlab/alert_management/payload/generic_spec.rb b/spec/lib/gitlab/alert_management/payload/generic_spec.rb index 3683dcd752f..b7660462b0d 100644 --- a/spec/lib/gitlab/alert_management/payload/generic_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/generic_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do subject { parsed_payload.starts_at } around do |example| - Timecop.freeze(current_time) { example.run } + travel_to(current_time) { example.run } end context 'without start_time' do @@ -99,7 +99,7 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do subject { parsed_payload.ends_at } around do |example| - Timecop.freeze(current_time) { example.run } + travel_to(current_time) { example.run } end context 'without end_time' do diff --git a/spec/lib/gitlab/analytics/unique_visits_spec.rb b/spec/lib/gitlab/analytics/unique_visits_spec.rb index 1432c9ac58f..6ac58e13f4c 100644 --- a/spec/lib/gitlab/analytics/unique_visits_spec.rb +++ b/spec/lib/gitlab/analytics/unique_visits_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state # Without freezing the time, the test may behave inconsistently # depending on which day of the week test is run. reference_time = Time.utc(2020, 6, 1) - Timecop.freeze(reference_time) { example.run } + travel_to(reference_time) { example.run } end describe '#track_visit' do diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb index 60b403780c0..ffd7813190a 100644 --- a/spec/lib/gitlab/auth/current_user_mode_spec.rb +++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb @@ -121,7 +121,7 @@ RSpec.describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_ subject.enable_admin_mode!(password: user.password) expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present' - Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do + travel_to(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do # in the future this will be a new request, simulate by clearing the RequestStore Gitlab::SafeRequestStore.clear! diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index d4483bf1754..b723c31c4aa 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -312,7 +312,7 @@ RSpec.describe Gitlab::BitbucketImport::Importer do # attributes later. existing_label.reload - Timecop.freeze(Time.now + 1.minute) do + travel_to(Time.now + 1.minute) do importer.execute label_after_import = project.labels.find(existing_label.id) diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index f724825a9cc..dd27b4045c9 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -82,7 +82,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when PST (Pacific Standard Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do + travel_to(Time.utc(2017, 1, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -90,7 +90,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when PDT (Pacific Daylight Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do + travel_to(Time.utc(2017, 6, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -117,7 +117,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when CET (Central European Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do + travel_to(Time.utc(2017, 1, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -125,7 +125,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when CEST (Central European Summer Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do + travel_to(Time.utc(2017, 6, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -152,7 +152,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when EST (Eastern Standard Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do + travel_to(Time.utc(2017, 1, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -160,7 +160,7 @@ RSpec.describe Gitlab::Ci::CronParser do context 'when EDT (Eastern Daylight Time)' do it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do + travel_to(Time.utc(2017, 6, 1)) do expect(subject.hour).to eq(hour_in_utc) end end @@ -174,7 +174,7 @@ RSpec.describe Gitlab::Ci::CronParser do # (e.g. America/Chicago) at the start of the test. Stubbing # TZ doesn't appear to be enough. it 'generates day without TZInfo::AmbiguousTime error' do - Timecop.freeze(Time.utc(2020, 1, 1)) do + travel_to(Time.utc(2020, 1, 1)) do expect(subject.year).to eq(year) expect(subject.month).to eq(12) expect(subject.day).to eq(1) diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb index a1a8fdecb85..9acaa57ee10 100644 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -7,7 +7,7 @@ require 'gitlab/danger/roulette' RSpec.describe Gitlab::Danger::Roulette do around do |example| - Timecop.freeze(Time.utc(2020, 06, 22, 10)) { example.run } + travel_to(Time.utc(2020, 06, 22, 10)) { example.run } end let(:backend_available) { true } diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb index 6fd32493d6b..5a47d74a7f3 100644 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -149,7 +149,7 @@ RSpec.describe Gitlab::Danger::Teammate do describe '#local_hour' do around do |example| - Timecop.freeze(Time.utc(2020, 6, 23, 10)) { example.run } + travel_to(Time.utc(2020, 6, 23, 10)) { example.run } end context 'when author is given' do diff --git a/spec/lib/gitlab/database/background_migration_job_spec.rb b/spec/lib/gitlab/database/background_migration_job_spec.rb index dd5bf8b512f..42695925a1c 100644 --- a/spec/lib/gitlab/database/background_migration_job_spec.rb +++ b/spec/lib/gitlab/database/background_migration_job_spec.rb @@ -85,7 +85,7 @@ RSpec.describe Gitlab::Database::BackgroundMigrationJob do let!(:job1) { create(:background_migration_job, :succeeded, created_at: initial_time, updated_at: initial_time) } it 'does not update non-pending jobs' do - Timecop.freeze(initial_time + 1.day) do + travel_to(initial_time + 1.day) do expect { described_class.mark_all_as_succeeded('TestJob', [1, 100]) } .to change { described_class.succeeded.count }.from(1).to(2) end diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb index 034bf966db7..8a35d8149ad 100644 --- a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb +++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Gitlab::Database::ObsoleteIgnoredColumns do describe '#execute' do it 'returns a list of class names and columns pairs' do - Timecop.freeze(REMOVE_DATE) do + travel_to(REMOVE_DATE) do expect(subject.execute).to eq([ ['Testing::A', { 'unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0'), diff --git a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb index 334cac653cf..885eef5723e 100644 --- a/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb +++ b/spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do let(:partitioning_key) { :created_at } around do |example| - Timecop.freeze(Date.parse('2020-08-22')) { example.run } + travel_to(Date.parse('2020-08-22')) { example.run } end context 'with existing partitions' do diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index bfbd7d1879b..147637cf471 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -213,7 +213,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'creates partitions including the next month from today' do today = Date.new(2020, 5, 8) - Timecop.freeze(today) do + travel_to(today) do migration.partition_table_by_date source_table, partition_column, min_date: min_date expect_range_partitions_for(partitioned_table, { @@ -233,7 +233,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe context 'without min_date, max_date' do it 'creates partitions for the current and next month' do current_date = Date.new(2020, 05, 22) - Timecop.freeze(current_date.to_time) do + travel_to(current_date.to_time) do migration.partition_table_by_date source_table, partition_column expect_range_partitions_for(partitioned_table, { diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index e1bcf4aeeb1..9271f635b14 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -85,9 +85,9 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do } end - let(:stale_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } - let(:active_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } - let(:future_sha) { Timecop.freeze(100.days.since) { create_commit } } + let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } + let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } + let(:future_sha) { travel_to(100.days.since) { create_commit } } before do repository.create_branch('stale-1', stale_sha) diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb index e68c1446502..9538c4bae2b 100644 --- a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb +++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do end it 'returns the correct duration in seconds' do - Timecop.freeze(start_time) do + travel_to(start_time) do subject.before expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 1.hour.to_f }) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 204fbff0c08..3bb9e52ef50 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -51,6 +51,7 @@ issues: - status_page_published_incident - namespace - note_authors +- issue_email_participants events: - author - project diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 9b8b2c1417a..4b40e8960b2 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -104,7 +104,7 @@ RSpec.describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do # Needs to be at least LfsToken::DEFAULT_EXPIRE_TIME + 60 seconds # in order to check whether it is valid 1 minute after it has expired - Timecop.freeze(Time.now + described_class::DEFAULT_EXPIRE_TIME + 60) do + travel_to(Time.now + described_class::DEFAULT_EXPIRE_TIME + 60) do expect(lfs_token.token_valid?(expired_token)).to be false end end diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb index cdb48024531..a9dae72f4db 100644 --- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Middleware::RailsQueueDuration do expect(transaction).to receive(:observe).with(:gitlab_rails_queue_duration_seconds, 1) - Timecop.freeze(Time.at(3)) do + travel_to(Time.at(3)) do expect(middleware.call(env)).to eq('yay') end end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb index 8abc944eeb1..b2350eff9f9 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do around do |example| - Timecop.freeze(Time.local(2008, 9, 1, 12, 0, 0)) { example.run } + travel_to(Time.local(2008, 9, 1, 12, 0, 0)) { example.run } end include_examples 'additional metrics query' do diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb index 4683c4eae28..66b93d0dd72 100644 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Prometheus::Queries::DeploymentQuery do around do |example| time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0) - Timecop.freeze(time_without_subsecond_values) { example.run } + travel_to(time_without_subsecond_values) { example.run } end it 'sends appropriate queries to prometheus' do diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index f0bf7b9964f..6ddeaf98370 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Tracking do end around do |example| - Timecop.freeze(timestamp) { example.run } + travel_to(timestamp) { example.run } end before do diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 6d4f52974aa..3255e3616b2 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s # depending on which day of the week test is run. # Monday 6th of June reference_time = Time.utc(2020, 6, 1) - Timecop.freeze(reference_time) { example.run } + travel_to(reference_time) { example.run } end describe '.categories' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 57de6e49a08..984fd4c08e6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -285,17 +285,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do cluster = create(:cluster, user: user) create(:project, creator: user) create(:clusters_applications_prometheus, :installed, cluster: cluster) + create(:project_tracing_setting) end expect(described_class.usage_activity_by_stage_monitor({})).to include( clusters: 2, clusters_applications_prometheus: 2, - operations_dashboard_default_dashboard: 2 + operations_dashboard_default_dashboard: 2, + projects_with_tracing_enabled: 2 ) expect(described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)).to include( clusters: 1, clusters_applications_prometheus: 1, - operations_dashboard_default_dashboard: 1 + operations_dashboard_default_dashboard: 1, + projects_with_tracing_enabled: 1 ) end end @@ -445,6 +448,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:projects_inheriting_instance_mattermost_active]).to eq(1) expect(count_data[:projects_with_repositories_enabled]).to eq(3) expect(count_data[:projects_with_error_tracking_enabled]).to eq(1) + expect(count_data[:projects_with_tracing_enabled]).to eq(1) expect(count_data[:projects_with_alerts_service_enabled]).to eq(1) expect(count_data[:projects_with_prometheus_alerts]).to eq(2) expect(count_data[:projects_with_terraform_reports]).to eq(2) diff --git a/spec/lib/grafana/time_window_spec.rb b/spec/lib/grafana/time_window_spec.rb index 9ee65c6cf20..0657bed7b28 100644 --- a/spec/lib/grafana/time_window_spec.rb +++ b/spec/lib/grafana/time_window_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Grafana::TimeWindow do let(:to) { '1552828200000' } around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end describe '#formatted' do @@ -37,7 +37,7 @@ RSpec.describe Grafana::RangeWithDefaults do let(:to) { Grafana::Timestamp.from_ms_since_epoch('1552828200000') } around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end describe '#to_hash' do @@ -82,7 +82,7 @@ RSpec.describe Grafana::Timestamp do let(:timestamp) { Time.at(1552799400) } around do |example| - Timecop.freeze(Time.utc(2019, 3, 17, 13, 10)) { example.run } + travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run } end describe '#formatted' do diff --git a/spec/migrations/backfill_status_page_published_incidents_spec.rb b/spec/migrations/backfill_status_page_published_incidents_spec.rb index 2b1ab891038..674484cdf0a 100644 --- a/spec/migrations/backfill_status_page_published_incidents_spec.rb +++ b/spec/migrations/backfill_status_page_published_incidents_spec.rb @@ -37,7 +37,7 @@ RSpec.describe BackfillStatusPagePublishedIncidents, :migration do end it 'creates a StatusPage::PublishedIncident record for each published issue' do - Timecop.freeze(current_time) do + travel_to(current_time) do expect(incidents.all).to be_empty migrate! diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb index 831895cb528..f51381f7a5f 100644 --- a/spec/models/ci/freeze_period_status_spec.rb +++ b/spec/models/ci/freeze_period_status_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Ci::FreezePeriodStatus do shared_examples 'within freeze period' do |time| it 'is frozen' do - Timecop.freeze(time) do + travel_to(time) do expect(subject).to be_truthy end end @@ -19,7 +19,7 @@ RSpec.describe Ci::FreezePeriodStatus do shared_examples 'outside freeze period' do |time| it 'is not frozen' do - Timecop.freeze(time) do + travel_to(time) do expect(subject).to be_falsy end end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 949d5f7bd04..cec3b544e50 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Ci::PipelineSchedule do subject { described_class.runnable_schedules } let!(:pipeline_schedule) do - Timecop.freeze(1.day.ago) do + travel_to(1.day.ago) do create(:ci_pipeline_schedule, :hourly) end end @@ -118,7 +118,7 @@ RSpec.describe Ci::PipelineSchedule do let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) } it "updates next_run_at to the sidekiq worker's execution time" do - Timecop.freeze(Time.zone.parse("2019-06-01 12:18:00+0000")) do + travel_to(Time.zone.parse("2019-06-01 12:18:00+0000")) do expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at) end end diff --git a/spec/models/ci_platform_metric_spec.rb b/spec/models/ci_platform_metric_spec.rb index 0b00875df43..f73db713791 100644 --- a/spec/models/ci_platform_metric_spec.rb +++ b/spec/models/ci_platform_metric_spec.rb @@ -45,7 +45,7 @@ RSpec.describe CiPlatformMetric do let(:tomorrow) { today + 1.day } it 'inserts platform target counts for that day' do - Timecop.freeze(today) do + travel_to(today) do create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS') create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS') create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') @@ -53,7 +53,7 @@ RSpec.describe CiPlatformMetric do create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') described_class.insert_auto_devops_platform_targets! end - Timecop.freeze(tomorrow) do + travel_to(tomorrow) do create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FARGATE') described_class.insert_auto_devops_platform_targets! end @@ -69,7 +69,7 @@ RSpec.describe CiPlatformMetric do let(:today) { Time.zone.local(1982, 4, 24) } it 'ignores those values' do - Timecop.freeze(today) do + travel_to(today) do create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'ECS') create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'FOO') create(:ci_variable, key: described_class::CI_VARIABLE_KEY, value: 'BAR') diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb index c91ddfee944..c0e5ddc23b1 100644 --- a/spec/models/concerns/resolvable_discussion_spec.rb +++ b/spec/models/concerns/resolvable_discussion_spec.rb @@ -553,13 +553,13 @@ RSpec.describe Discussion, ResolvableDiscussion do let(:time) { Time.current.utc } before do - Timecop.freeze(time - 1.second) do + travel_to(time - 1.second) do first_note.resolve!(current_user) end - Timecop.freeze(time) do + travel_to(time) do third_note.resolve!(current_user) end - Timecop.freeze(time + 1.second) do + travel_to(time + 1.second) do second_note.resolve!(current_user) end end diff --git a/spec/models/concerns/schedulable_spec.rb b/spec/models/concerns/schedulable_spec.rb index 875c2d80e55..62acd12e267 100644 --- a/spec/models/concerns/schedulable_spec.rb +++ b/spec/models/concerns/schedulable_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Schedulable do context 'for a pipeline_schedule' do # let! is used to reset the next_run_at value before each spec let(:object) do - Timecop.freeze(1.day.ago) do + travel_to(1.day.ago) do create(:ci_pipeline_schedule, :hourly) end end diff --git a/spec/models/import_failure_spec.rb b/spec/models/import_failure_spec.rb index cdef125e890..9fee1b0ae7b 100644 --- a/spec/models/import_failure_spec.rb +++ b/spec/models/import_failure_spec.rb @@ -16,7 +16,7 @@ RSpec.describe ImportFailure do it 'orders hard failures by newest first' do older_failure = hard_failure.dup - Timecop.freeze(1.day.before(hard_failure.created_at)) do + travel_to(1.day.before(hard_failure.created_at)) do older_failure.save! expect(ImportFailure.hard_failures_by_correlation_id(correlation_id)).to eq([hard_failure, older_failure]) diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb index 966e4321378..1d3c09a48b7 100644 --- a/spec/models/issue/metrics_spec.rb +++ b/spec/models/issue/metrics_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Issue::Metrics do context "milestones" do it "records the first time an issue is associated with a milestone" do time = Time.current - Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) } + travel_to(time) { subject.update(milestone: create(:milestone, project: project)) } metrics = subject.metrics expect(metrics).to be_present @@ -47,9 +47,9 @@ RSpec.describe Issue::Metrics do it "does not record the second time an issue is associated with a milestone" do time = Time.current - Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) } - Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) } - Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone, project: project)) } + travel_to(time) { subject.update(milestone: create(:milestone, project: project)) } + travel_to(time + 2.hours) { subject.update(milestone: nil) } + travel_to(time + 6.hours) { subject.update(milestone: create(:milestone, project: project)) } metrics = subject.metrics expect(metrics).to be_present @@ -61,7 +61,7 @@ RSpec.describe Issue::Metrics do it "records the first time an issue is associated with a list label" do list_label = create(:list).label time = Time.current - Timecop.freeze(time) { subject.update(label_ids: [list_label.id]) } + travel_to(time) { subject.update(label_ids: [list_label.id]) } metrics = subject.metrics expect(metrics).to be_present @@ -71,9 +71,9 @@ RSpec.describe Issue::Metrics do it "does not record the second time an issue is associated with a list label" do time = Time.current first_list_label = create(:list).label - Timecop.freeze(time) { subject.update(label_ids: [first_list_label.id]) } + travel_to(time) { subject.update(label_ids: [first_list_label.id]) } second_list_label = create(:list).label - Timecop.freeze(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) } + travel_to(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) } metrics = subject.metrics expect(metrics).to be_present diff --git a/spec/models/issue_email_participant_spec.rb b/spec/models/issue_email_participant_spec.rb new file mode 100644 index 00000000000..f19e65e31f3 --- /dev/null +++ b/spec/models/issue_email_participant_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IssueEmailParticipant do + describe "Associations" do + it { is_expected.to belong_to(:issue) } + end + + describe 'Validations' do + subject { build(:issue_email_participant) } + + it { is_expected.to validate_presence_of(:issue) } + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to validate_uniqueness_of(:email).scoped_to([:issue_id]) } + + it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index b830ce23c1c..16ea2989eda 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -28,6 +28,7 @@ RSpec.describe Issue do it { is_expected.to have_and_belong_to_many(:prometheus_alert_events) } it { is_expected.to have_and_belong_to_many(:self_managed_prometheus_alert_events) } it { is_expected.to have_many(:prometheus_alerts) } + it { is_expected.to have_many(:issue_email_participants) } describe 'versions.most_recent' do it 'returns the most recent version' do diff --git a/spec/models/project_feature_usage_spec.rb b/spec/models/project_feature_usage_spec.rb index 908b98ee9c2..d55d41fab85 100644 --- a/spec/models/project_feature_usage_spec.rb +++ b/spec/models/project_feature_usage_spec.rb @@ -48,7 +48,7 @@ RSpec.describe ProjectFeatureUsage, type: :model do feature_usage.log_jira_dvcs_integration_usage first_logged_at = feature_usage.jira_dvcs_cloud_last_sync_at - Timecop.freeze(1.hour.from_now) do + travel_to(1.hour.from_now) do ProjectFeatureUsage.new(project_id: project.id).log_jira_dvcs_integration_usage end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 96645005d0a..7fdc1dd12c1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -72,6 +72,7 @@ RSpec.describe Project do it { is_expected.to have_one(:last_event).class_name('Event') } it { is_expected.to have_one(:forked_from_project).through(:fork_network_member) } it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') } + it { is_expected.to have_one(:tracing_setting).class_name('ProjectTracingSetting') } it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') } it { is_expected.to have_one(:project_setting) } it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') } diff --git a/spec/models/project_tracing_setting_spec.rb b/spec/models/project_tracing_setting_spec.rb new file mode 100644 index 00000000000..a7e4e557b25 --- /dev/null +++ b/spec/models/project_tracing_setting_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ProjectTracingSetting do + describe '#external_url' do + let_it_be(:project) { create(:project) } + + let(:tracing_setting) { project.build_tracing_setting } + + describe 'Validations' do + describe 'external_url' do + it 'accepts a valid url' do + tracing_setting.external_url = 'https://gitlab.com' + + expect(tracing_setting).to be_valid + end + + it 'fails with an invalid url' do + tracing_setting.external_url = 'gitlab.com' + + expect(tracing_setting).to be_invalid + end + + it 'fails with a blank string' do + tracing_setting.external_url = nil + + expect(tracing_setting).to be_invalid + end + + it 'sanitizes the url' do + tracing_setting.external_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>} + + expect(tracing_setting).to be_valid + expect(tracing_setting.external_url).to eq(%{https://replaceme.com/'>}) + end + end + end + end +end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 0e3473ec0d1..a9c4c6680cd 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -443,7 +443,7 @@ RSpec.describe Todo do it 'updates updated_at' do create(:todo, :pending) - Timecop.freeze(1.day.from_now) do + travel_to(1.day.from_now) do expected_update_date = Time.current.utc ids = described_class.batch_update(state: :done) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0f71c7790d4..af5614ba85e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3902,7 +3902,7 @@ RSpec.describe User do it 'changes the namespace (just to compare to when username is not changed)' do expect do - Timecop.freeze(1.second.from_now) do + travel_to(1.second.from_now) do user.update!(username: new_username) end end.to change { user.namespace.updated_at } @@ -4899,7 +4899,7 @@ RSpec.describe User do user.block end - it { is_expected.to eq User::BLOCKED_MESSAGE } + it { is_expected.to eq :blocked } end context 'when user is an internal user' do @@ -4907,7 +4907,7 @@ RSpec.describe User do user.update(user_type: :ghost) end - it { is_expected.to be User::LOGIN_FORBIDDEN } + it { is_expected.to be :forbidden } end context 'when user is locked' do diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 779ae983886..01ead9eef54 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -259,7 +259,7 @@ RSpec.describe API::Releases do end it '#collected_at' do - Timecop.freeze(Time.now.round) do + travel_to(Time.now.round) do get api("/projects/#{project.id}/releases/v0.1", maintainer) expect(json_response['evidences'].first['collected_at'].to_datetime.to_i).to be_within(1.minute).of(release.evidences.first.created_at.to_i) @@ -476,7 +476,7 @@ RSpec.describe API::Releases do it 'sets the released_at to the current time if the released_at parameter is not provided' do now = Time.zone.parse('2015-08-25 06:00:00Z') - Timecop.freeze(now) do + travel_to(now) do post api("/projects/#{project.id}/releases", maintainer), params: params expect(project.releases.last.released_at).to eq(now) diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 4338bfa3759..3f57b8ba67b 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'value stream analytics events' do project.add_developer(user) 3.times do |count| - Timecop.freeze(Time.now + count.days) do + travel_to(Time.now + count.days) do create_cycle end end diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb index 7f9999bf3d2..72689595480 100644 --- a/spec/requests/request_profiler_spec.rb +++ b/spec/requests/request_profiler_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Request Profiler' do time = Time.now path = "/#{project.full_path}" - Timecop.freeze(time) do + travel_to(time) do get path, params: {}, headers: { 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token, 'X-Profile-Mode' => profile_type } end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 51741440075..4c1e698d52d 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -154,7 +154,7 @@ RSpec.describe Ci::RetryBuildService do describe '#execute' do let(:new_build) do - Timecop.freeze(1.second.from_now) do + travel_to(1.second.from_now) do service.execute(build) end end @@ -257,7 +257,7 @@ RSpec.describe Ci::RetryBuildService do describe '#reprocess' do let(:new_build) do - Timecop.freeze(1.second.from_now) do + travel_to(1.second.from_now) do service.reprocess!(build) end end diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb index 80735985406..aa1de368154 100644 --- a/spec/services/ci/update_build_state_service_spec.rb +++ b/spec/services/ci/update_build_state_service_spec.rb @@ -83,6 +83,23 @@ RSpec.describe Ci::UpdateBuildStateService do { checksum: 'crc32:12345678', state: 'failed', failure_reason: 'script_failure' } end + context 'when build does not have associated trace chunks' do + it 'updates a build status' do + result = subject.execute + + expect(build).to be_failed + expect(result.status).to eq 200 + end + + it 'does not increment invalid trace metric' do + execute_with_stubbed_metrics! + + expect(metrics) + .not_to have_received(:increment_trace_operation) + .with(operation: :invalid) + end + end + context 'when build trace has been migrated' do before do create(:ci_build_trace_chunk, :persisted, build: build, initial_data: 'abcd') diff --git a/spec/services/deployments/after_create_service_spec.rb b/spec/services/deployments/after_create_service_spec.rb index 6cdb4c88191..ee9b008c2f8 100644 --- a/spec/services/deployments/after_create_service_spec.rb +++ b/spec/services/deployments/after_create_service_spec.rb @@ -269,14 +269,14 @@ RSpec.describe Deployments::AfterCreateService do it "does not overwrite the older 'first_deployed_to_production_at' time" do # Previous deploy time = 5.minutes.from_now - Timecop.freeze(time) { service.execute } + travel_to(time) { service.execute } expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at previous_time = merge_request.reload.metrics.first_deployed_to_production_at # Current deploy - Timecop.freeze(time + 12.hours) { service.execute } + travel_to(time + 12.hours) { service.execute } expect(merge_request.reload.metrics.first_deployed_to_production_at).to eq(previous_time) end diff --git a/spec/services/design_management/copy_design_collection/copy_service_spec.rb b/spec/services/design_management/copy_design_collection/copy_service_spec.rb index 09a4df59c6a..e93e5f13fea 100644 --- a/spec/services/design_management/copy_design_collection/copy_service_spec.rb +++ b/spec/services/design_management/copy_design_collection/copy_service_spec.rb @@ -133,18 +133,26 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla end it 'copies design notes correctly', :aggregate_failures, :sidekiq_inline do - note = create(:diff_note_on_design, noteable: designs.first, project: project) + old_notes = [ + create(:diff_note_on_design, note: 'first note', noteable: designs.first, project: project, author: create(:user)), + create(:diff_note_on_design, note: 'second note', noteable: designs.first, project: project, author: create(:user)) + ] + matchers = old_notes.map do |note| + have_attributes( + note.attributes.slice( + :type, + :author_id, + :note, + :position + ) + ) + end - expect { subject }.to change { Note.count }.by(1) + expect { subject }.to change { Note.count }.by(2) - new_note = target_issue.designs.first.notes.first + new_notes = target_issue.designs.first.notes.fresh - expect(new_note).to have_attributes( - type: note.type, - author_id: note.author_id, - note: note.note, - position: note.position - ) + expect(new_notes).to match_array(matchers) end it 'links the LfsObjects' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index b3e8fba4e9a..cfda27795c7 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -650,7 +650,7 @@ RSpec.describe Issues::UpdateService, :mailer do context 'when the labels change' do before do - Timecop.freeze(1.minute.from_now) do + travel_to(1.minute.from_now) do update_issue(label_ids: [label.id]) end end diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb index 82b6b05975b..a2cd5ffdd38 100644 --- a/spec/services/keys/last_used_service_spec.rb +++ b/spec/services/keys/last_used_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Keys::LastUsedService do key = create(:key, last_used_at: 1.year.ago) time = Time.zone.now - Timecop.freeze(time) { described_class.new(key).execute } + travel_to(time) { described_class.new(key).execute } expect(key.reload.last_used_at).to be_like_time(time) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 84ca77b4ad5..ed8872b71f7 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -583,7 +583,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do context 'when the labels change' do before do - Timecop.freeze(1.minute.from_now) do + travel_to(1.minute.from_now) do update_merge_request({ label_ids: [label.id] }) end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 7c0d4b756bd..4da9f4115a1 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -437,7 +437,7 @@ RSpec.describe Notes::CreateService do expect do existing_note - Timecop.freeze(Time.current + 1.minute) { subject } + travel_to(Time.current + 1.minute) { subject } existing_note.reload end.to change { existing_note.type }.from(nil).to('DiscussionNote') diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb index 47b8ba0cd72..66efdf8abe7 100644 --- a/spec/services/notes/update_service_spec.rb +++ b/spec/services/notes/update_service_spec.rb @@ -42,7 +42,7 @@ RSpec.describe Notes::UpdateService do end it 'does not update the note when params is blank' do - Timecop.freeze(1.day.from_now) do + travel_to(1.day.from_now) do expect { update_note({}) }.not_to change { note.reload.updated_at } end end diff --git a/spec/services/system_notes/incident_service_spec.rb b/spec/services/system_notes/incident_service_spec.rb index 1906d890305..ab9b9eb2bd4 100644 --- a/spec/services/system_notes/incident_service_spec.rb +++ b/spec/services/system_notes/incident_service_spec.rb @@ -15,64 +15,44 @@ RSpec.describe ::SystemNotes::IncidentService do allow(Gitlab::AppLogger).to receive(:error).and_call_original end - context 'with add_severity_system_note feature flag enabled' do - before do - stub_feature_flags(add_severity_system_note: project) - end - - it_behaves_like 'a system note' do - let(:action) { 'severity' } - end - - IssuableSeverity.severities.keys.each do |severity| - context "with #{severity} severity" do - before do - issuable_severity.update!(severity: severity) - end - - it 'has the appropriate message' do - severity_label = IssuableSeverity::SEVERITY_LABELS.fetch(severity.to_sym) - - expect(change_severity.note).to eq("changed the severity to **#{severity_label}**") - end - end - end - - context 'when severity is invalid' do - let(:invalid_severity) { 'invalid-severity' } + it_behaves_like 'a system note' do + let(:action) { 'severity' } + end + IssuableSeverity.severities.keys.each do |severity| + context "with #{severity} severity" do before do - allow(noteable).to receive(:severity).and_return(invalid_severity) + issuable_severity.update!(severity: severity) end - it 'does not create system note' do - expect { change_severity }.not_to change { noteable.notes.count } - end + it 'has the appropriate message' do + severity_label = IssuableSeverity::SEVERITY_LABELS.fetch(severity.to_sym) - it 'writes error to logs' do - change_severity - - expect(Gitlab::AppLogger).to have_received(:error).with( - message: 'Cannot create a system note for severity change', - noteable_class: noteable.class.to_s, - noteable_id: noteable.id, - severity: invalid_severity - ) + expect(change_severity.note).to eq("changed the severity to **#{severity_label}**") end end end - context 'with add_severity_system_note feature flag disabled' do + context 'when severity is invalid' do + let(:invalid_severity) { 'invalid-severity' } + before do - stub_feature_flags(add_severity_system_note: false) + allow(noteable).to receive(:severity).and_return(invalid_severity) end it 'does not create system note' do expect { change_severity }.not_to change { noteable.notes.count } end - it 'does not write error to logs' do - expect(Gitlab::AppLogger).not_to have_received(:error) + it 'writes error to logs' do + change_severity + + expect(Gitlab::AppLogger).to have_received(:error).with( + message: 'Cannot create a system note for severity change', + noteable_class: noteable.class.to_s, + noteable_id: noteable.id, + severity: invalid_severity + ) end end end diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 27ac8562b76..17e806d21d9 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -99,6 +99,7 @@ module UsageDataHelpers projects_with_error_tracking_enabled projects_with_alerts_service_enabled projects_with_prometheus_alerts + projects_with_tracing_enabled projects_with_expiration_policy_enabled projects_with_expiration_policy_disabled projects_with_expiration_policy_enabled_with_keep_n_unset diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb index 7f26155f9d6..3f147f942ba 100644 --- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb +++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb @@ -59,7 +59,7 @@ RSpec.shared_examples 'known sign in' do it 'notifies the user when the cookie is expired' do stub_cookie - Timecop.freeze((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do + travel_to((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do expect_next_instance_of(NotificationService) do |instance| expect(instance).to receive(:unknown_sign_in) end diff --git a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb index 14b851d2828..e869cbce6ae 100644 --- a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb +++ b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb @@ -13,8 +13,8 @@ RSpec.shared_examples 'throttled touch' do first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2) second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5) - Timecop.freeze(first_updated_at) { subject.touch } - Timecop.freeze(second_updated_at) { subject.touch } + travel_to(first_updated_at) { subject.touch } + travel_to(second_updated_at) { subject.touch } expect(subject.updated_at).to be_like_time(first_updated_at) end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 50d164d1705..cb626b08456 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -290,7 +290,7 @@ RSpec.describe PostReceive do # MySQL drops milliseconds in the timestamps, so advance at least # a second to ensure we see changes. - Timecop.freeze(1.second.from_now) do + travel_to(1.second.from_now) do expect do perform project.reload |