summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js6
-rw-r--r--app/controllers/invites_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb8
-rw-r--r--app/graphql/mutations/award_emojis/toggle.rb2
-rw-r--r--app/graphql/types/base_field.rb28
-rw-r--r--app/helpers/search_helper.rb15
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/services/audit_event_service.rb34
-rw-r--r--app/views/groups/_import_group_pane.html.haml2
-rw-r--r--changelogs/unreleased/241359-nomethoderror-error.yml5
-rw-r--r--changelogs/unreleased/249091-autocomplete-recent-mrs.yml5
-rw-r--r--changelogs/unreleased/dblessing-auth-events-logging.yml5
-rw-r--r--db/fixtures/development/02_users.rb12
-rw-r--r--doc/user/group/settings/img/import_panel_v13_1.pngbin23446 -> 0 bytes
-rw-r--r--doc/user/group/settings/img/import_panel_v13_4.pngbin0 -> 23373 bytes
-rw-r--r--doc/user/group/settings/import_export.md2
-rw-r--r--doc/user/search/index.md1
-rw-r--r--lib/gitlab/search/recent_issues.rb45
-rw-r--r--lib/gitlab/search/recent_items.rb59
-rw-r--r--lib/gitlab/search/recent_merge_requests.rb17
-rw-r--r--locale/gitlab.pot4
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb5
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb5
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb3
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb10
-rw-r--r--spec/controllers/sessions_controller_spec.rb17
-rw-r--r--spec/features/groups/import_export/import_file_spec.rb2
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js9
-rw-r--r--spec/helpers/search_helper_spec.rb33
-rw-r--r--spec/lib/gitlab/search/recent_issues_spec.rb84
-rw-r--r--spec/lib/gitlab/search/recent_merge_requests_spec.rb11
-rw-r--r--spec/services/audit_event_service_spec.rb16
-rw-r--r--spec/support/shared_examples/lib/gitlab/search/recent_items.rb87
35 files changed, 408 insertions, 135 deletions
diff --git a/Gemfile b/Gemfile
index 8825c529a9d..90abfd30900 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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
deleted file mode 100644
index ce2eb579446..00000000000
--- a/doc/user/group/settings/img/import_panel_v13_1.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/settings/img/import_panel_v13_4.png b/doc/user/group/settings/img/import_panel_v13_4.png
new file mode 100644
index 00000000000..e4e5b0e91a1
--- /dev/null
+++ b/doc/user/group/settings/img/import_panel_v13_4.png
Binary files differ
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