diff options
35 files changed, 408 insertions, 135 deletions
@@ -89,7 +89,7 @@ gem 'grape-entity', '~> 0.7.1' gem 'rack-cors', '~> 1.0.6', require: 'rack/cors' # GraphQL API -gem 'graphql', '~> 1.10.5' +gem 'graphql', '~> 1.11.4' # NOTE: graphiql-rails v1.5+ doesn't work: https://gitlab.com/gitlab-org/gitlab/issues/31771 # TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released: # https://gitlab.com/gitlab-org/gitlab/issues/31747 diff --git a/Gemfile.lock b/Gemfile.lock index d728925c507..7d743f2029e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -505,7 +505,7 @@ GEM graphiql-rails (1.4.10) railties sprockets-rails - graphql (1.10.5) + graphql (1.11.4) graphql-docs (1.6.0) commonmarker (~> 0.16) escape_utils (~> 1.2) @@ -1346,7 +1346,7 @@ DEPENDENCIES grape-path-helpers (~> 1.3) grape_logging (~> 1.7) graphiql-rails (~> 1.4.10) - graphql (~> 1.10.5) + graphql (~> 1.11.4) graphql-docs (~> 1.6.0) grpc (~> 1.30.2) gssapi diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 26ac98c877e..e9c3fe0a406 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -75,7 +75,7 @@ export function getParameterValues(sParam, url = window.location) { * @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs */ export function mergeUrlParams(params, url, options = {}) { - const { spreadArrays = false } = options; + const { spreadArrays = false, sort = false } = options; const re = /^([^?#]*)(\?[^#]*)?(.*)/; let merged = {}; const [, fullpath, query, fragment] = url.match(re); @@ -108,7 +108,9 @@ export function mergeUrlParams(params, url, options = {}) { Object.assign(merged, params); - const newQuery = Object.keys(merged) + const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged); + + const newQuery = mergedKeys .filter(key => merged[key] !== null) .map(key => { let value = merged[key]; diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index e65b27db3ae..aa9c7d01ba3 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -76,8 +76,10 @@ class InvitesController < ApplicationController notice << "or create an account" if Gitlab::CurrentSettings.allow_signup? notice = notice.join(' ') + "." + # this is temporary finder instead of using member method due to render_404 possibility + # will be resolved via https://gitlab.com/gitlab-org/gitlab/-/issues/245325 initial_member = Member.find_by_invite_token(params[:id]) - redirect_params = initial_member ? { invite_email: member.invite_email } : {} + redirect_params = initial_member ? { invite_email: initial_member.invite_email } : {} store_location_for :user, request.fullpath diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d4b40eda77d..92785540172 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -50,6 +50,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] + after_action :log_merge_request_show, only: [:show] + feature_category :source_code_management, unless: -> (action) { action.ends_with?("_reports") } feature_category :code_testing, @@ -450,6 +452,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end end + def log_merge_request_show + return unless current_user && @merge_request + + ::Gitlab::Search::RecentMergeRequests.new(user: current_user).log_view(@merge_request) + end + def authorize_read_actual_head_pipeline! return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline) end diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb index a7714e695d2..679ec7a14ff 100644 --- a/app/graphql/mutations/award_emojis/toggle.rb +++ b/app/graphql/mutations/award_emojis/toggle.rb @@ -5,7 +5,7 @@ module Mutations class Toggle < Base graphql_name 'AwardEmojiToggle' - field :toggledOn, GraphQL::BOOLEAN_TYPE, null: false, + field :toggled_on, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates the status of the emoji. ' \ 'True if the toggle awarded the emoji, and false if the toggle removed the emoji.' diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 90f5004dd8f..1e72a4cddf5 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -18,6 +18,34 @@ module Types super(*args, **kwargs, &block) end + # Based on https://github.com/rmosolgo/graphql-ruby/blob/v1.11.4/lib/graphql/schema/field.rb#L538-L563 + # Modified to fix https://github.com/rmosolgo/graphql-ruby/issues/3113 + def resolve_field(obj, args, ctx) + ctx.schema.after_lazy(obj) do |after_obj| + query_ctx = ctx.query.context + inner_obj = after_obj && after_obj.object + + ctx.schema.after_lazy(to_ruby_args(after_obj, args, ctx)) do |ruby_args| + if authorized?(inner_obj, ruby_args, query_ctx) + if @resolve_proc + # We pass `after_obj` here instead of `inner_obj` because extensions expect a GraphQL::Schema::Object + with_extensions(after_obj, ruby_args, query_ctx) do |extended_obj, extended_args| + # Since `extended_obj` is now a GraphQL::Schema::Object, we need to get the inner object and pass that to `@resolve_proc` + extended_obj = extended_obj.object if extended_obj.is_a?(GraphQL::Schema::Object) + + @resolve_proc.call(extended_obj, args, ctx) + end + else + public_send_field(after_obj, ruby_args, query_ctx) + end + else + err = GraphQL::UnauthorizedFieldError.new(object: inner_obj, type: obj.class, context: ctx, field: self) + query_ctx.schema.unauthorized_field(err) + end + end + end + end + def base_complexity complexity = DEFAULT_COMPLEXITY complexity += 1 if calls_gitaly? diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9f3623ad511..d55ad878b92 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -7,6 +7,7 @@ module SearchHelper return unless current_user resources_results = [ + recent_merge_requests_autocomplete(term), recent_issues_autocomplete(term), groups_autocomplete(term), projects_autocomplete(term) @@ -180,6 +181,20 @@ module SearchHelper end end + def recent_merge_requests_autocomplete(term, limit = 5) + return [] unless current_user + + ::Gitlab::Search::RecentMergeRequests.new(user: current_user).search(term).limit(limit).map do |mr| + { + category: "Recent merge requests", + id: mr.id, + label: search_result_sanitize(mr.title), + url: merge_request_path(mr), + avatar_url: mr.project.avatar_url || '' + } + end + end + def recent_issues_autocomplete(term, limit = 5) return [] unless current_user diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6da32f5cb62..3fdc501644d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -21,6 +21,7 @@ class MergeRequest < ApplicationRecord include MilestoneEventable include StateEventable include ApprovableBase + include IdInOrdered extend ::Gitlab::Utils::Override diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb index c5d28317685..d7630dbdac9 100644 --- a/app/services/audit_event_service.rb +++ b/app/services/audit_event_service.rb @@ -25,6 +25,8 @@ class AuditEventService # # @return [AuditEventService] def for_authentication + mark_as_authentication_event! + @details = { with: @details[:with], target_id: @author.id, @@ -40,6 +42,7 @@ class AuditEventService # @return [AuditEvent] persited if saves and non-persisted if fails def security_event log_security_event_to_file + log_authentication_event_to_database log_security_event_to_database end @@ -50,6 +53,7 @@ class AuditEventService private + attr_accessor :authentication_event attr_reader :ip_address def build_author(author) @@ -70,6 +74,22 @@ class AuditEventService } end + def authentication_event_payload + { + # @author can be a User or various Gitlab::Audit authors. + # Only capture real users for successful authentication events. + user: author_if_user, + user_name: @author.name, + ip_address: ip_address, + result: AuthenticationEvent.results[:success], + provider: @details[:with] + } + end + + def author_if_user + @author if @author.is_a?(User) + end + def file_logger @file_logger ||= Gitlab::AuditJsonLogger.build end @@ -78,11 +98,25 @@ class AuditEventService @details.merge(@details.slice(:from, :to).transform_values(&:to_s)) end + def mark_as_authentication_event! + self.authentication_event = true + end + + def authentication_event? + authentication_event + end + def log_security_event_to_database return if Gitlab::Database.read_only? AuditEvent.create(base_payload.merge(details: @details)) end + + def log_authentication_event_to_database + return unless Gitlab::Database.read_write? && authentication_event? + + AuthenticationEvent.create(authentication_event_payload) + end end AuditEventService.prepend_if_ee('EE::AuditEventService') diff --git a/app/views/groups/_import_group_pane.html.haml b/app/views/groups/_import_group_pane.html.haml index adfac7d59a5..9ad8ebbb37d 100644 --- a/app/views/groups/_import_group_pane.html.haml +++ b/app/views/groups/_import_group_pane.html.haml @@ -41,7 +41,7 @@ = s_('GroupsNew|To copy a GitLab group between installations, navigate to the group settings page for the original installation, generate an export file, and upload it here.') .row .form-group.col-sm-12 - = f.label :file, s_('GroupsNew|GitLab group export'), class: 'label-bold' + = f.label :file, s_('GroupsNew|Import a GitLab group export file'), class: 'label-bold' %div = render 'shared/file_picker_button', f: f, field: :file, help_text: nil diff --git a/changelogs/unreleased/241359-nomethoderror-error.yml b/changelogs/unreleased/241359-nomethoderror-error.yml new file mode 100644 index 00000000000..abd3c27e62b --- /dev/null +++ b/changelogs/unreleased/241359-nomethoderror-error.yml @@ -0,0 +1,5 @@ +--- +title: 'Do not raise error when a member is not found by invite token' +merge_request: 42349 +author: +type: fixed diff --git a/changelogs/unreleased/249091-autocomplete-recent-mrs.yml b/changelogs/unreleased/249091-autocomplete-recent-mrs.yml new file mode 100644 index 00000000000..5a67e136ca4 --- /dev/null +++ b/changelogs/unreleased/249091-autocomplete-recent-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Add autocomplete search suggestions for recent merge requests +merge_request: 42560 +author: +type: added diff --git a/changelogs/unreleased/dblessing-auth-events-logging.yml b/changelogs/unreleased/dblessing-auth-events-logging.yml new file mode 100644 index 00000000000..2c1340cc7af --- /dev/null +++ b/changelogs/unreleased/dblessing-auth-events-logging.yml @@ -0,0 +1,5 @@ +--- +title: Log authentication events alongside existing audit events +merge_request: 42033 +author: +type: added diff --git a/db/fixtures/development/02_users.rb b/db/fixtures/development/02_users.rb index 909d10cbb40..7916cdd5fb1 100644 --- a/db/fixtures/development/02_users.rb +++ b/db/fixtures/development/02_users.rb @@ -22,7 +22,7 @@ class Gitlab::Seeder::Users private def create_mass_users! - encrypted_password = Devise::Encryptor.digest(User, '12345678') + encrypted_password = Devise::Encryptor.digest(User, random_password) Gitlab::Seeder.with_mass_insert(MASS_USERS_COUNT, User) do ActiveRecord::Base.connection.execute <<~SQL @@ -49,6 +49,10 @@ class Gitlab::Seeder::Users FROM users WHERE NOT admin SQL end + + puts '===========================================================' + puts "INFO: Password for newly created users is: #{random_password}" + puts '===========================================================' end def create_random_users! @@ -59,7 +63,7 @@ class Gitlab::Seeder::Users name: FFaker::Name.name, email: FFaker::Internet.email, confirmed_at: DateTime.now, - password: '12345678' + password: random_password ) print '.' @@ -68,6 +72,10 @@ class Gitlab::Seeder::Users end end end + + def random_password + @random_password ||= SecureRandom.hex.slice(0,16) + end end Gitlab::Seeder.quiet do diff --git a/doc/user/group/settings/img/import_panel_v13_1.png b/doc/user/group/settings/img/import_panel_v13_1.png Binary files differdeleted file mode 100644 index ce2eb579446..00000000000 --- a/doc/user/group/settings/img/import_panel_v13_1.png +++ /dev/null diff --git a/doc/user/group/settings/img/import_panel_v13_4.png b/doc/user/group/settings/img/import_panel_v13_4.png Binary files differnew file mode 100644 index 00000000000..e4e5b0e91a1 --- /dev/null +++ b/doc/user/group/settings/img/import_panel_v13_4.png diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md index 37e33da1d80..77cb862a49d 100644 --- a/doc/user/group/settings/import_export.md +++ b/doc/user/group/settings/import_export.md @@ -94,7 +94,7 @@ on an existing group's page. 1. On the New Group page, select the **Import group** tab. - ![Fill in group details](img/import_panel_v13_1.png) + ![Fill in group details](img/import_panel_v13_4.png) 1. Enter your group name. diff --git a/doc/user/search/index.md b/doc/user/search/index.md index b5b9c9b40d5..475a72385ac 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -171,6 +171,7 @@ You can also type in this search bar to see autocomplete suggestions for: - Project feature pages (try and type **milestones**) - Various settings pages (try and type **user settings**) - Recently viewed issues (try and type some word from the title of a recently viewed issue) +- Recently viewed merge requests (try and type some word from the title of a recently merge request) ## To-Do List diff --git a/lib/gitlab/search/recent_issues.rb b/lib/gitlab/search/recent_issues.rb index 3eb7101a344..413218da64d 100644 --- a/lib/gitlab/search/recent_issues.rb +++ b/lib/gitlab/search/recent_issues.rb @@ -2,51 +2,16 @@ module Gitlab module Search - class RecentIssues - ITEMS_LIMIT = 100 - EXPIRES_AFTER = 7.days - - def initialize(user:, items_limit: ITEMS_LIMIT, expires_after: EXPIRES_AFTER) - @user = user - @items_limit = items_limit - @expires_after = expires_after - end - - def log_view(issue) - with_redis do |redis| - redis.zadd(key, Time.now.to_f, issue.id) - redis.expire(key, @expires_after) - - # There is a race condition here where we could end up removing an - # item from 2 places concurrently but this is fine since worst case - # scenario we remove an extra item from the end of the list. - if redis.zcard(key) > @items_limit - redis.zremrangebyrank(key, 0, 0) # Remove least recent - end - end - end - - def search(term) - ids = with_redis do |redis| - redis.zrevrange(key, 0, @items_limit - 1) - end.map(&:to_i) - - IssuesFinder.new(@user, search: term, in: 'title').execute.reorder(nil).id_in_ordered(ids) # rubocop: disable CodeReuse/ActiveRecord - end - + class RecentIssues < RecentItems private - def with_redis(&blk) - Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord - end - - def key - "recent_items:#{type.name.downcase}:#{@user.id}" - end - def type Issue end + + def finder + IssuesFinder + end end end end diff --git a/lib/gitlab/search/recent_items.rb b/lib/gitlab/search/recent_items.rb new file mode 100644 index 00000000000..40d96ded275 --- /dev/null +++ b/lib/gitlab/search/recent_items.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Gitlab + module Search + # This is an abstract class used for storing/searching recently viewed + # items. The #type and #finder methods are the only ones needed to be + # implemented by classes inheriting from this. + class RecentItems + ITEMS_LIMIT = 100 + EXPIRES_AFTER = 7.days + + def initialize(user:, items_limit: ITEMS_LIMIT, expires_after: EXPIRES_AFTER) + @user = user + @items_limit = items_limit + @expires_after = expires_after + end + + def log_view(item) + with_redis do |redis| + redis.zadd(key, Time.now.to_f, item.id) + redis.expire(key, @expires_after) + + # There is a race condition here where we could end up removing an + # item from 2 places concurrently but this is fine since worst case + # scenario we remove an extra item from the end of the list. + if redis.zcard(key) > @items_limit + redis.zremrangebyrank(key, 0, 0) # Remove least recent + end + end + end + + def search(term) + ids = with_redis do |redis| + redis.zrevrange(key, 0, @items_limit - 1) + end.map(&:to_i) + + finder.new(@user, search: term, in: 'title').execute.reorder(nil).id_in_ordered(ids) # rubocop: disable CodeReuse/ActiveRecord + end + + private + + def with_redis(&blk) + Gitlab::Redis::SharedState.with(&blk) # rubocop: disable CodeReuse/ActiveRecord + end + + def key + "recent_items:#{type.name.downcase}:#{@user.id}" + end + + def type + raise NotImplementedError + end + + def finder + raise NotImplementedError + end + end + end +end diff --git a/lib/gitlab/search/recent_merge_requests.rb b/lib/gitlab/search/recent_merge_requests.rb new file mode 100644 index 00000000000..7b14e3b33e5 --- /dev/null +++ b/lib/gitlab/search/recent_merge_requests.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Search + class RecentMergeRequests < RecentItems + private + + def type + MergeRequest + end + + def finder + MergeRequestsFinder + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a9f8aba3f9a..3f4b8095997 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12755,10 +12755,10 @@ msgstr "" msgid "GroupsNew|Create group" msgstr "" -msgid "GroupsNew|GitLab group export" +msgid "GroupsNew|Import" msgstr "" -msgid "GroupsNew|Import" +msgid "GroupsNew|Import a GitLab group export file" msgstr "" msgid "GroupsNew|Import group" diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb index 2de824bbf3c..ecff173b8ac 100644 --- a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb @@ -11,6 +11,11 @@ RSpec.describe Ldap::OmniauthCallbacksController do expect(request.env['warden']).to be_authenticated end + it 'creates an authentication event record' do + expect { post provider }.to change { AuthenticationEvent.count }.by(1) + expect(AuthenticationEvent.last.provider).to eq(provider.to_s) + end + context 'with sign in prevented' do let(:ldap_settings) { ldap_setting_defaults.merge(prevent_ldap_sign_in: true) } diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index de6edbe936d..291d51348e6 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -170,6 +170,11 @@ RSpec.describe OmniauthCallbacksController, type: :controller do expect(request.env['warden']).to be_authenticated end + it 'creates an authentication event record' do + expect { post provider }.to change { AuthenticationEvent.count }.by(1) + expect(AuthenticationEvent.last.provider).to eq(provider.to_s) + end + context 'when user has no linked provider' do let(:user) { create(:user) } diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index ef72416fc71..ed5198bf015 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1029,7 +1029,8 @@ RSpec.describe Projects::IssuesController do go(id: issue.to_param) - expect(recent_issues_double).to have_received(:log_view) + expect(response).to be_successful + expect(recent_issues_double).to have_received(:log_view).with(issue) end context 'when not logged in' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index db97a962fbc..ee194e5ff2f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -123,6 +123,16 @@ RSpec.describe Projects::MergeRequestsController do expect(response).to be_successful end + it 'logs the view with Gitlab::Search::RecentMergeRequests' do + recent_merge_requests_double = instance_double(::Gitlab::Search::RecentMergeRequests, log_view: nil) + expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests_double) + + go(format: :html) + + expect(response).to be_successful + expect(recent_merge_requests_double).to have_received(:log_view).with(merge_request) + end + context "that is invalid" do let(:merge_request) { create(:invalid_merge_request, target_project: project, source_project: project) } diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 2c3815b36af..688539f2a03 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -140,6 +140,11 @@ RSpec.describe SessionsController do expect(AuditEvent.last.details[:with]).to eq('standard') end + it 'creates an authentication event record' do + expect { post(:create, params: { user: user_params }) }.to change { AuthenticationEvent.count }.by(1) + expect(AuthenticationEvent.last.provider).to eq('standard') + end + include_examples 'user login request with unique ip limit', 302 do def request post(:create, params: { user: user_params }) @@ -407,6 +412,11 @@ RSpec.describe SessionsController do expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { AuditEvent.count }.by(1) expect(AuditEvent.last.details[:with]).to eq("two-factor") end + + it "creates an authentication event record" do + expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { AuthenticationEvent.count }.by(1) + expect(AuthenticationEvent.last.provider).to eq("two-factor") + end end context 'when using two-factor authentication via U2F device' do @@ -448,6 +458,13 @@ RSpec.describe SessionsController do expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { AuditEvent.count }.by(1) expect(AuditEvent.last.details[:with]).to eq("two-factor-via-u2f-device") end + + it "creates an authentication event record" do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + + expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { AuthenticationEvent.count }.by(1) + expect(AuthenticationEvent.last.provider).to eq("two-factor-via-u2f-device") + end end end diff --git a/spec/features/groups/import_export/import_file_spec.rb b/spec/features/groups/import_export/import_file_spec.rb index ee4f2740f9f..f117b5d56e9 100644 --- a/spec/features/groups/import_export/import_file_spec.rb +++ b/spec/features/groups/import_export/import_file_spec.rb @@ -32,7 +32,7 @@ RSpec.describe 'Import/Export - Group Import', :js do fill_in :group_name, with: group_name find('#import-group-tab').click - expect(page).to have_content 'GitLab group export' + expect(page).to have_content 'Import a GitLab group export file' attach_file(file) do find('.js-filepicker-button').click end diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index dbb126e0ad1..869ae274a3f 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -161,6 +161,15 @@ describe('URL utility', () => { ); }); + it('sorts params in alphabetical order with sort option', () => { + expect(mergeUrlParams({ c: 'c', b: 'b', a: 'a' }, 'https://host/path', { sort: true })).toBe( + 'https://host/path?a=a&b=b&c=c', + ); + expect( + mergeUrlParams({ alpha: 'alpha' }, 'https://host/path?op=/&foo=bar', { sort: true }), + ).toBe('https://host/path?alpha=alpha&foo=bar&op=%2F'); + }); + describe('with spread array option', () => { const spreadArrayOptions = { spreadArrays: true }; diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index a39ef8634df..594c5c11994 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -106,6 +106,39 @@ RSpec.describe SearchHelper do }) end + it 'includes the first 5 of the users recent merge requests' do + recent_merge_requests = instance_double(::Gitlab::Search::RecentMergeRequests) + expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests) + project1 = create(:project, :with_avatar, namespace: user.namespace) + project2 = create(:project, namespace: user.namespace) + merge_request1 = create(:merge_request, :unique_branches, title: 'Merge request 1', target_project: project1, source_project: project1) + merge_request2 = create(:merge_request, :unique_branches, title: 'Merge request 2', target_project: project2, source_project: project2) + + other_merge_requests = create_list(:merge_request, 5) + + expect(recent_merge_requests).to receive(:search).with('the search term').and_return(MergeRequest.id_in_ordered([merge_request1.id, merge_request2.id, *other_merge_requests.map(&:id)])) + + results = search_autocomplete_opts("the search term") + + expect(results.count).to eq(5) + + expect(results[0]).to include({ + category: 'Recent merge requests', + id: merge_request1.id, + label: 'Merge request 1', + url: Gitlab::Routing.url_helpers.project_merge_request_path(merge_request1.project, merge_request1), + avatar_url: project1.avatar_url + }) + + expect(results[1]).to include({ + category: 'Recent merge requests', + id: merge_request2.id, + label: 'Merge request 2', + url: Gitlab::Routing.url_helpers.project_merge_request_path(merge_request2.project, merge_request2), + avatar_url: '' # This project didn't have an avatar so set this to '' + }) + end + it "does not include the public group" do group = create(:group) expect(search_autocomplete_opts(group.name).size).to eq(0) diff --git a/spec/lib/gitlab/search/recent_issues_spec.rb b/spec/lib/gitlab/search/recent_issues_spec.rb index 1822c971a72..19a41d2aa38 100644 --- a/spec/lib/gitlab/search/recent_issues_spec.rb +++ b/spec/lib/gitlab/search/recent_issues_spec.rb @@ -2,86 +2,10 @@ require 'spec_helper' -RSpec.describe ::Gitlab::Search::RecentIssues, :clean_gitlab_redis_shared_state do - let(:user) { create(:user) } - let(:issue) { create(:issue, title: 'hello world 1', project: project) } - let(:recent_issues) { described_class.new(user: user, items_limit: 5) } - let(:project) { create(:project, :public) } - - describe '#log_viewing' do - it 'adds the item to the recent items' do - recent_issues.log_view(issue) - - results = recent_issues.search('hello') - - expect(results).to eq([issue]) - end - - it 'removes an item when it exceeds the size items_limit' do - (1..6).each do |i| - recent_issues.log_view(create(:issue, title: "issue #{i}", project: project)) - end - - results = recent_issues.search('issue') - - expect(results.map(&:title)).to contain_exactly('issue 6', 'issue 5', 'issue 4', 'issue 3', 'issue 2') - end - - it 'expires the items after expires_after' do - recent_issues = described_class.new(user: user, expires_after: 0) - - recent_issues.log_view(issue) - - results = recent_issues.search('hello') - - expect(results).to be_empty - end - - it 'does not include results logged for another user' do - another_user = create(:user) - another_issue = create(:issue, title: 'hello world 2', project: project) - described_class.new(user: another_user).log_view(another_issue) - recent_issues.log_view(issue) - - results = recent_issues.search('hello') - - expect(results).to eq([issue]) - end +RSpec.describe ::Gitlab::Search::RecentIssues do + def create_item(content:, project:) + create(:issue, title: content, project: project) end - describe '#search' do - let(:issue1) { create(:issue, title: "matching issue 1", project: project) } - let(:issue2) { create(:issue, title: "matching issue 2", project: project) } - let(:issue3) { create(:issue, title: "matching issue 3", project: project) } - let(:non_matching_issue) { create(:issue, title: "different issue", project: project) } - let!(:non_viewed_issued) { create(:issue, title: "matching but not viewed issue", project: project) } - - before do - recent_issues.log_view(issue1) - recent_issues.log_view(issue2) - recent_issues.log_view(issue3) - recent_issues.log_view(non_matching_issue) - end - - it 'matches partial text in the issue title' do - expect(recent_issues.search('matching')).to contain_exactly(issue1, issue2, issue3) - end - - it 'returns results sorted by recently viewed' do - recent_issues.log_view(issue2) - - expect(recent_issues.search('matching')).to eq([issue2, issue3, issue1]) - end - - it 'does not leak issues you no longer have access to' do - private_project = create(:project, :public, namespace: create(:group)) - private_issue = create(:issue, project: private_project, title: 'matching issue title') - - recent_issues.log_view(private_issue) - - private_project.update!(visibility_level: Project::PRIVATE) - - expect(recent_issues.search('matching')).not_to include(private_issue) - end - end + it_behaves_like 'search recent items' end diff --git a/spec/lib/gitlab/search/recent_merge_requests_spec.rb b/spec/lib/gitlab/search/recent_merge_requests_spec.rb new file mode 100644 index 00000000000..c6678ce0342 --- /dev/null +++ b/spec/lib/gitlab/search/recent_merge_requests_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Search::RecentMergeRequests do + def create_item(content:, project:) + create(:merge_request, :unique_branches, title: content, target_project: project, source_project: project) + end + + it_behaves_like 'search recent items' +end diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb index 5059727ac4a..93de2a23edc 100644 --- a/spec/services/audit_event_service_spec.rb +++ b/spec/services/audit_event_service_spec.rb @@ -52,6 +52,22 @@ RSpec.describe AuditEventService do expect(details[:action]).to eq(:create) expect(details[:target_id]).to eq(1) end + + context 'authentication event' do + let(:audit_service) { described_class.new(user, user, with: 'standard') } + + it 'creates an authentication event' do + expect(AuthenticationEvent).to receive(:create).with( + user: user, + user_name: user.name, + ip_address: user.current_sign_in_ip, + result: AuthenticationEvent.results[:success], + provider: 'standard' + ) + + audit_service.for_authentication.security_event + end + end end describe '#log_security_event_to_file' do diff --git a/spec/support/shared_examples/lib/gitlab/search/recent_items.rb b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb new file mode 100644 index 00000000000..f96ff4b101e --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'search recent items' do + let_it_be(:user) { create(:user) } + let_it_be(:recent_items) { described_class.new(user: user, items_limit: 5) } + let(:item) { create_item(content: 'hello world 1', project: project) } + let(:project) { create(:project, :public) } + + describe '#log_view', :clean_gitlab_redis_shared_state do + it 'adds the item to the recent items' do + recent_items.log_view(item) + + results = recent_items.search('hello') + + expect(results).to eq([item]) + end + + it 'removes an item when it exceeds the size items_limit' do + (1..6).each do |i| + recent_items.log_view(create_item(content: "item #{i}", project: project)) + end + + results = recent_items.search('item') + + expect(results.map(&:title)).to contain_exactly('item 6', 'item 5', 'item 4', 'item 3', 'item 2') + end + + it 'expires the items after expires_after' do + recent_items = described_class.new(user: user, expires_after: 0) + + recent_items.log_view(item) + + results = recent_items.search('hello') + + expect(results).to be_empty + end + + it 'does not include results logged for another user' do + another_user = create(:user) + another_item = create_item(content: 'hello world 2', project: project) + described_class.new(user: another_user).log_view(another_item) + recent_items.log_view(item) + + results = recent_items.search('hello') + + expect(results).to eq([item]) + end + end + + describe '#search', :clean_gitlab_redis_shared_state do + let(:item1) { create_item(content: "matching item 1", project: project) } + let(:item2) { create_item(content: "matching item 2", project: project) } + let(:item3) { create_item(content: "matching item 3", project: project) } + let(:non_matching_item) { create_item(content: "different item", project: project) } + let!(:non_viewed_item) { create_item(content: "matching but not viewed item", project: project) } + + before do + recent_items.log_view(item1) + recent_items.log_view(item2) + recent_items.log_view(item3) + recent_items.log_view(non_matching_item) + end + + it 'matches partial text in the item title' do + expect(recent_items.search('matching')).to contain_exactly(item1, item2, item3) + end + + it 'returns results sorted by recently viewed' do + recent_items.log_view(item2) + + expect(recent_items.search('matching')).to eq([item2, item3, item1]) + end + + it 'does not leak items you no longer have access to' do + private_project = create(:project, :public, namespace: create(:group)) + private_item = create_item(content: 'matching item title', project: private_project) + + recent_items.log_view(private_item) + + private_project.update!(visibility_level: Project::PRIVATE) + + expect(recent_items.search('matching')).not_to include(private_item) + end + end +end |