Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9bc5f183df
commit
b89bcf56ec
67 changed files with 895 additions and 210 deletions
2
Gemfile
2
Gemfile
|
@ -488,7 +488,7 @@ gem 'flipper', '~> 0.17.1'
|
|||
gem 'flipper-active_record', '~> 0.17.1'
|
||||
gem 'flipper-active_support_cache_store', '~> 0.17.1'
|
||||
gem 'unleash', '~> 0.1.5'
|
||||
gem 'gitlab-experiment', '~> 0.5.2'
|
||||
gem 'gitlab-experiment', '~> 0.5.3'
|
||||
|
||||
# Structured logging
|
||||
gem 'lograge', '~> 0.5'
|
||||
|
|
|
@ -443,7 +443,7 @@ GEM
|
|||
numerizer (~> 0.2)
|
||||
gitlab-dangerfiles (1.1.1)
|
||||
danger-gitlab
|
||||
gitlab-experiment (0.5.2)
|
||||
gitlab-experiment (0.5.3)
|
||||
activesupport (>= 3.0)
|
||||
scientist (~> 1.6, >= 1.6.0)
|
||||
gitlab-fog-azure-rm (1.0.1)
|
||||
|
@ -1424,7 +1424,7 @@ DEPENDENCIES
|
|||
github-markup (~> 1.7.0)
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-dangerfiles (~> 1.1.1)
|
||||
gitlab-experiment (~> 0.5.2)
|
||||
gitlab-experiment (~> 0.5.3)
|
||||
gitlab-fog-azure-rm (~> 1.0.1)
|
||||
gitlab-fog-google (~> 1.13)
|
||||
gitlab-labkit (~> 0.16.2)
|
||||
|
|
|
@ -793,10 +793,22 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
|
|||
commit(types.VIEW_DIFF_FILE, fileHash);
|
||||
};
|
||||
|
||||
export const setFileByFile = ({ commit }, { fileByFile }) => {
|
||||
export const setFileByFile = ({ state, commit }, { fileByFile }) => {
|
||||
const fileViewMode = fileByFile ? DIFF_VIEW_FILE_BY_FILE : DIFF_VIEW_ALL_FILES;
|
||||
commit(types.SET_FILE_BY_FILE, fileByFile);
|
||||
Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode);
|
||||
|
||||
return axios
|
||||
.put(state.endpointUpdateUser, {
|
||||
view_diffs_file_by_file: fileByFile,
|
||||
})
|
||||
.then(() => {
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/326961
|
||||
// We can't even do a simple console warning here because
|
||||
// the pipeline will fail. However, the issue above will
|
||||
// eventually handle errors appropriately.
|
||||
// console.warn('Saving the file-by-fil user preference failed.');
|
||||
});
|
||||
};
|
||||
|
||||
export function reviewFile({ commit, state }, { file, reviewed = true }) {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import { experiment } from '~/experimentation/utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
import { NEW_REPO_EXPERIMENT } from '../constants';
|
||||
import blankProjectIllustration from '../illustrations/blank-project.svg';
|
||||
import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
|
||||
import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
|
||||
|
@ -13,8 +14,10 @@ import WelcomePage from './welcome.vue';
|
|||
const BLANK_PANEL = 'blank_project';
|
||||
const CI_CD_PANEL = 'cicd_for_external_repo';
|
||||
const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
|
||||
|
||||
const PANELS = [
|
||||
{
|
||||
key: 'blank',
|
||||
name: BLANK_PANEL,
|
||||
selector: '#blank-project-pane',
|
||||
title: s__('ProjectsNew|Create blank project'),
|
||||
|
@ -24,6 +27,7 @@ const PANELS = [
|
|||
illustration: blankProjectIllustration,
|
||||
},
|
||||
{
|
||||
key: 'template',
|
||||
name: 'create_from_template',
|
||||
selector: '#create-from-template-pane',
|
||||
title: s__('ProjectsNew|Create from template'),
|
||||
|
@ -33,6 +37,7 @@ const PANELS = [
|
|||
illustration: createFromTemplateIllustration,
|
||||
},
|
||||
{
|
||||
key: 'import',
|
||||
name: 'import_project',
|
||||
selector: '#import-project-pane',
|
||||
title: s__('ProjectsNew|Import project'),
|
||||
|
@ -42,6 +47,7 @@ const PANELS = [
|
|||
illustration: importProjectIllustration,
|
||||
},
|
||||
{
|
||||
key: 'ci',
|
||||
name: CI_CD_PANEL,
|
||||
selector: '#ci-cd-project-pane',
|
||||
title: s__('ProjectsNew|Run CI/CD for external repository'),
|
||||
|
@ -86,11 +92,27 @@ export default {
|
|||
|
||||
computed: {
|
||||
availablePanels() {
|
||||
const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
|
||||
use: () => ({
|
||||
blank: s__('ProjectsNew|Create blank project'),
|
||||
import: s__('ProjectsNew|Import project'),
|
||||
}),
|
||||
try: () => ({
|
||||
blank: s__('ProjectsNew|Create blank project/repository'),
|
||||
import: s__('ProjectsNew|Import project/repository'),
|
||||
}),
|
||||
});
|
||||
|
||||
const updatedPanels = PANELS.map(({ key, title, ...el }) => ({
|
||||
...el,
|
||||
title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title,
|
||||
}));
|
||||
|
||||
if (this.isCiCdAvailable) {
|
||||
return PANELS;
|
||||
return updatedPanels;
|
||||
}
|
||||
|
||||
return PANELS.filter((p) => p.name !== CI_CD_PANEL);
|
||||
return updatedPanels.filter((p) => p.name !== CI_CD_PANEL);
|
||||
},
|
||||
|
||||
activePanel() {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import Tracking from '~/tracking';
|
||||
import { NEW_REPO_EXPERIMENT } from '../constants';
|
||||
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
|
||||
|
||||
const trackingMixin = Tracking.mixin(gon.tracking_data);
|
||||
const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const NEW_REPO_EXPERIMENT = 'new_repo';
|
|
@ -74,6 +74,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
|
||||
|
||||
if @project.saved?
|
||||
experiment(:new_repo, user: current_user).track(:project_created)
|
||||
experiment(:new_project_readme, actor: current_user).track(
|
||||
:created,
|
||||
property: active_new_project_tab,
|
||||
|
|
|
@ -75,7 +75,7 @@ module Emails
|
|||
end
|
||||
|
||||
def ssh_key_expired_email(user, fingerprints)
|
||||
return unless user && user.active?
|
||||
return unless user&.active?
|
||||
|
||||
@user = user
|
||||
@fingerprints = fingerprints
|
||||
|
@ -86,6 +86,18 @@ module Emails
|
|||
end
|
||||
end
|
||||
|
||||
def ssh_key_expiring_soon_email(user, fingerprints)
|
||||
return unless user&.active?
|
||||
|
||||
@user = user
|
||||
@fingerprints = fingerprints
|
||||
@target_url = profile_keys_url
|
||||
|
||||
Gitlab::I18n.with_locale(@user.preferred_language) do
|
||||
mail(to: @user.notification_email, subject: subject(_("Your SSH key is expiring soon.")))
|
||||
end
|
||||
end
|
||||
|
||||
def unknown_sign_in_email(user, ip, time)
|
||||
@user = user
|
||||
@ip = ip
|
||||
|
|
|
@ -44,6 +44,7 @@ class Key < ApplicationRecord
|
|||
scope :for_user, -> (user) { where(user: user) }
|
||||
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
|
||||
scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
|
||||
scope :expiring_soon_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') > CURRENT_DATE AND date(expires_at AT TIME ZONE 'UTC') < ? AND before_expiry_notification_delivered_at IS NULL", DAYS_TO_EXPIRE.days.from_now.to_date]) }
|
||||
|
||||
def self.regular_keys
|
||||
where(type: ['Key', nil])
|
||||
|
|
|
@ -11,14 +11,14 @@ class Service < ApplicationRecord
|
|||
include EachBatch
|
||||
|
||||
SERVICE_NAMES = %w[
|
||||
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
|
||||
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
|
||||
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
|
||||
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
|
||||
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
|
||||
].freeze
|
||||
|
||||
PROJECT_SPECIFIC_SERVICE_NAMES = %w[
|
||||
jenkins
|
||||
datadog jenkins
|
||||
].freeze
|
||||
|
||||
# Fake services to help with local development.
|
||||
|
|
|
@ -104,6 +104,7 @@ class User < ApplicationRecord
|
|||
# Profile
|
||||
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :expired_today_and_unnotified_keys, -> { expired_today_and_not_notified }, class_name: 'Key'
|
||||
has_many :expiring_soon_and_unnotified_keys, -> { expiring_soon_and_not_notified }, class_name: 'Key'
|
||||
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :group_deploy_keys
|
||||
has_many :gpg_keys
|
||||
|
@ -402,6 +403,14 @@ class User < ApplicationRecord
|
|||
.where('keys.user_id = users.id')
|
||||
.expired_today_and_not_notified)
|
||||
end
|
||||
scope :with_ssh_key_expiring_soon, -> do
|
||||
includes(:expiring_soon_and_unnotified_keys)
|
||||
.where('EXISTS (?)',
|
||||
::Key
|
||||
.select(1)
|
||||
.where('keys.user_id = users.id')
|
||||
.expiring_soon_and_not_notified)
|
||||
end
|
||||
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
|
||||
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
|
||||
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
|
||||
|
|
|
@ -114,6 +114,36 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
|
|||
presenter(merge_request).api_unapprove_path
|
||||
end
|
||||
|
||||
expose :test_reports_path do |merge_request|
|
||||
if merge_request.has_test_reports?
|
||||
test_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :accessibility_report_path do |merge_request|
|
||||
if merge_request.has_accessibility_reports?
|
||||
accessibility_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :codequality_reports_path do |merge_request|
|
||||
if merge_request.has_codequality_reports?
|
||||
codequality_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :terraform_reports_path do |merge_request|
|
||||
if merge_request.has_terraform_reports?
|
||||
terraform_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :exposed_artifacts_path do |merge_request|
|
||||
if merge_request.has_exposed_artifacts?
|
||||
exposed_artifacts_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :blob_path do
|
||||
expose :head_path, if: -> (mr, _) { mr.source_branch_sha } do |merge_request|
|
||||
project_blob_path(merge_request.project, merge_request.source_branch_sha)
|
||||
|
|
|
@ -76,36 +76,6 @@ class MergeRequestPollWidgetEntity < Grape::Entity
|
|||
presenter(merge_request).cancel_auto_merge_path
|
||||
end
|
||||
|
||||
expose :test_reports_path do |merge_request|
|
||||
if merge_request.has_test_reports?
|
||||
test_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :accessibility_report_path do |merge_request|
|
||||
if merge_request.has_accessibility_reports?
|
||||
accessibility_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :codequality_reports_path do |merge_request|
|
||||
if merge_request.has_codequality_reports?
|
||||
codequality_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :terraform_reports_path do |merge_request|
|
||||
if merge_request.has_terraform_reports?
|
||||
terraform_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :exposed_artifacts_path do |merge_request|
|
||||
if merge_request.has_exposed_artifacts?
|
||||
exposed_artifacts_project_merge_request_path(merge_request.project, merge_request, format: :json)
|
||||
end
|
||||
end
|
||||
|
||||
expose :create_issue_to_resolve_discussions_path do |merge_request|
|
||||
presenter(merge_request).create_issue_to_resolve_discussions_path
|
||||
end
|
||||
|
|
|
@ -2,17 +2,38 @@
|
|||
|
||||
module Keys
|
||||
class ExpiryNotificationService < ::Keys::BaseService
|
||||
attr_accessor :keys
|
||||
attr_accessor :keys, :expiring_soon
|
||||
|
||||
def initialize(user, params)
|
||||
@keys = params[:keys]
|
||||
@expiring_soon = params[:expiring_soon]
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def execute
|
||||
return unless user.can?(:receive_notifications)
|
||||
return unless allowed?
|
||||
|
||||
if expiring_soon
|
||||
trigger_expiring_soon_notification
|
||||
else
|
||||
trigger_expired_notification
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed?
|
||||
user.can?(:receive_notifications)
|
||||
end
|
||||
|
||||
def trigger_expiring_soon_notification
|
||||
notification_service.ssh_key_expiring_soon(user, keys.map(&:fingerprint))
|
||||
|
||||
keys.update_all(before_expiry_notification_delivered_at: Time.current.utc)
|
||||
end
|
||||
|
||||
def trigger_expired_notification
|
||||
notification_service.ssh_key_expired(user, keys.map(&:fingerprint))
|
||||
|
||||
keys.update_all(expiry_notification_delivered_at: Time.current.utc)
|
||||
|
|
|
@ -86,6 +86,13 @@ class NotificationService
|
|||
mailer.ssh_key_expired_email(user, fingerprints).deliver_later
|
||||
end
|
||||
|
||||
# Notify the user when at least one of their ssh key is expiring soon
|
||||
def ssh_key_expiring_soon(user, fingerprints)
|
||||
return unless user.can?(:receive_notifications)
|
||||
|
||||
mailer.ssh_key_expiring_soon_email(user, fingerprints).deliver_later
|
||||
end
|
||||
|
||||
# Notify a user when a previously unknown IP or device is used to
|
||||
# sign in to their account
|
||||
def unknown_sign_in(user, ip, time)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.omniauth-container.gl-mt-5
|
||||
%label.label-bold.d-block
|
||||
Sign in with
|
||||
= _('Sign in with')
|
||||
- providers = enabled_button_based_providers
|
||||
.d-flex.justify-content-between.flex-wrap
|
||||
- providers.each do |provider|
|
||||
|
@ -17,4 +17,4 @@
|
|||
%label
|
||||
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
|
||||
%span
|
||||
Remember me
|
||||
= _('Remember me')
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
|
||||
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
|
||||
|
||||
-# Rendering this above Gon, to use in JS later
|
||||
= render 'layouts/header/new_repo_experiment'
|
||||
= Gon::Base.render_data(nonce: content_security_policy_nonce)
|
||||
|
||||
= javascript_include_tag locale_path unless I18n.locale == :en
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_value: "" } }
|
||||
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
|
||||
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
|
||||
= sprite_icon('plus-square')
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
|
@ -37,8 +37,7 @@
|
|||
= render 'layouts/header/project_invite_members_new_dropdown_item'
|
||||
%li.divider
|
||||
%li.dropdown-bold-header GitLab
|
||||
- if current_user.can_create_project?
|
||||
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link'
|
||||
= content_for :new_repo_experiment
|
||||
- if current_user.can_create_group?
|
||||
%li= link_to _('New group'), new_group_path
|
||||
- if current_user.can?(:create_snippet)
|
7
app/views/layouts/header/_new_repo_experiment.html.haml
Normal file
7
app/views/layouts/header/_new_repo_experiment.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
- content_for :new_repo_experiment do
|
||||
- if current_user&.can_create_project?
|
||||
- experiment(:new_repo, user: current_user) do |e|
|
||||
- e.use do
|
||||
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
|
||||
- e.try do
|
||||
%li= link_to _('New project/repository'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
|
|
@ -2,7 +2,7 @@
|
|||
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
|
||||
%ul.list-unstyled.navbar-sub-nav
|
||||
- if dashboard_nav_link?(:projects)
|
||||
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do
|
||||
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }) do
|
||||
%button{ type: 'button', data: { toggle: "dropdown" } }
|
||||
= _('Projects')
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
|
|
|
@ -11,14 +11,21 @@
|
|||
= nav_link(path: 'projects#trending') do
|
||||
= link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
|
||||
= _('Explore projects')
|
||||
= nav_link(path: 'projects/new#blank_project',
|
||||
html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' },
|
||||
data: { track_label: "projects_dropdown_blank_project", track_event: "click_link" }) do
|
||||
= link_to new_project_path(anchor: 'blank_project') do
|
||||
= _('Create blank project')
|
||||
= nav_link(path: 'projects/new#import_project') do
|
||||
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link" } do
|
||||
= _('Import project')
|
||||
- experiment(:new_repo, user: current_user) do |e|
|
||||
- e.use do
|
||||
= nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
|
||||
= link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
|
||||
= _('Create blank project')
|
||||
= nav_link(path: 'projects/new#import_project') do
|
||||
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
|
||||
= _('Import project')
|
||||
- e.try do
|
||||
= nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
|
||||
= link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
|
||||
= _('Create blank project/repository')
|
||||
= nav_link(path: 'projects/new#import_project') do
|
||||
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
|
||||
= _('Import project/repository')
|
||||
= nav_link(path: 'projects/new#create_from_template') do
|
||||
= link_to new_project_path(anchor: 'create_from_template'), data: { track_label: "projects_dropdown_create_from_template", track_event: "click_link" } do
|
||||
= _('Create from template')
|
||||
|
|
9
app/views/notify/ssh_key_expiring_soon.text.erb
Normal file
9
app/views/notify/ssh_key_expiring_soon.text.erb
Normal file
|
@ -0,0 +1,9 @@
|
|||
<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
|
||||
|
||||
<%= _('Your SSH keys with the following fingerprints are scheduled to expire soon:') %>
|
||||
|
||||
<% @fingerprints.each do |fingerprint| %>
|
||||
- <%= fingerprint %>
|
||||
<% end %>
|
||||
|
||||
<%= _('You can create a new one or check them in your SSH keys settings %{ssh_key_link}.') % { ssh_key_link: @target_url } %>
|
13
app/views/notify/ssh_key_expiring_soon_email.html.haml
Normal file
13
app/views/notify/ssh_key_expiring_soon_email.html.haml
Normal file
|
@ -0,0 +1,13 @@
|
|||
%p
|
||||
= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
|
||||
%p
|
||||
= _('Your SSH keys with the following fingerprints are scheduled to expire soon:')
|
||||
%table
|
||||
%tbody
|
||||
- @fingerprints.each do |fingerprint|
|
||||
%tr
|
||||
%td= fingerprint
|
||||
|
||||
%p
|
||||
- ssh_key_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
|
||||
= html_escape(_('You can create a new one or check them in your %{ssh_key_link_start}SSH keys%{ssh_key_link_end} settings.')) % { ssh_key_link_start: ssh_key_link_start, ssh_key_link_end: '</a>'.html_safe }
|
|
@ -17,8 +17,9 @@
|
|||
- else
|
||||
= _('Project access token creation is disabled in this group. You can still use and manage existing tokens.')
|
||||
%p
|
||||
- if current_user.can?(:admin_group, @project.group)
|
||||
- group_settings_link = edit_group_path(@project.group)
|
||||
- root_group = @project.group.root_ancestor
|
||||
- if current_user.can?(:admin_group, root_group)
|
||||
- group_settings_link = edit_group_path(root_group)
|
||||
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link }
|
||||
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
|
||||
|
|
|
@ -451,6 +451,14 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:ssh_keys_expiring_soon_notification
|
||||
:feature_category: :compliance_management
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:stuck_ci_jobs
|
||||
:feature_category: :continuous_integration
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -17,7 +17,7 @@ module SshKeys
|
|||
|
||||
keys = user.expired_today_and_unnotified_keys
|
||||
|
||||
Keys::ExpiryNotificationService.new(user, { keys: keys }).execute
|
||||
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: false }).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
25
app/workers/ssh_keys/expiring_soon_notification_worker.rb
Normal file
25
app/workers/ssh_keys/expiring_soon_notification_worker.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SshKeys
|
||||
class ExpiringSoonNotificationWorker
|
||||
include ApplicationWorker
|
||||
include CronjobQueue
|
||||
|
||||
feature_category :compliance_management
|
||||
idempotent!
|
||||
|
||||
def perform
|
||||
return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml)
|
||||
|
||||
User.with_ssh_key_expiring_soon.find_each do |user|
|
||||
with_context(user: user) do
|
||||
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring soon ssh key(s)"
|
||||
|
||||
keys = user.expiring_soon_and_unnotified_keys
|
||||
|
||||
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: true }).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Partial index optimization for namespaces id
|
||||
merge_request: 58220
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in shared/_omniauth_box.html.haml
|
||||
merge_request: 58281
|
||||
author: nuwe1
|
||||
type: other
|
5
changelogs/unreleased/expiring-ssh-key-notification.yml
Normal file
5
changelogs/unreleased/expiring-ssh-key-notification.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: User notification when SSH key is set to expire soon
|
||||
merge_request: 58171
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move CI related paths to cached MR widget
|
||||
merge_request: 58711
|
||||
author:
|
||||
type: performance
|
5
changelogs/unreleased/sfang-fix-group-settings-link.yml
Normal file
5
changelogs/unreleased/sfang-fix-group-settings-link.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix project access token creation group settings link
|
||||
merge_request: 58686
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Sync single-file mode user preference when changed from the MR cog menu checkbox
|
||||
merge_request: 55931
|
||||
author:
|
||||
type: changed
|
8
config/feature_flags/experiment/new_repo.yml
Normal file
8
config/feature_flags/experiment/new_repo.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: new_repo
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55818
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285153
|
||||
milestone: '13.11'
|
||||
type: experiment
|
||||
group: group::adoption
|
||||
default_enabled: false
|
|
@ -569,6 +569,9 @@ Settings.cron_jobs['ssh_keys_expired_notification_worker']['job_class'] = 'SshKe
|
|||
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *'
|
||||
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker'
|
||||
Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker']['cron'] ||= '0 1 * * *'
|
||||
Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker']['job_class'] = 'SshKeys::ExpiringSoonNotificationWorker'
|
||||
|
||||
Gitlab.com do
|
||||
Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({})
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddBeforeExpiryNotificationDeliveredToKeys < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :keys, :before_expiry_notification_delivered_at, :datetime_with_timezone
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToKeysOnExpiresAtAndBeforeExpiryNotificationUndelivered < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'idx_keys_expires_at_and_before_expiry_notification_undelivered'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :keys,
|
||||
"date(timezone('UTC', expires_at)), before_expiry_notification_delivered_at",
|
||||
where: 'before_expiry_notification_delivered_at IS NULL', name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:keys, INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateNamespacesIdParentIdPartialIndex < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
NAME = 'index_namespaces_id_parent_id_is_null'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :namespaces, :id, where: 'parent_id IS NULL', name: NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :namespaces, :id, name: NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210401175134
Normal file
1
db/schema_migrations/20210401175134
Normal file
|
@ -0,0 +1 @@
|
|||
07d527134f776dbed2199f1717c34b3a6c41caadcaa3c50e6e5866f2cfad31b0
|
1
db/schema_migrations/20210401192808
Normal file
1
db/schema_migrations/20210401192808
Normal file
|
@ -0,0 +1 @@
|
|||
1cd4799ed7df41bfb9d96a7d18faaa9cbb2dc03f2a804c2bc3c1a6bba15d6d3d
|
1
db/schema_migrations/20210406063442
Normal file
1
db/schema_migrations/20210406063442
Normal file
|
@ -0,0 +1 @@
|
|||
d29f002f88440a10674b251791fa027cb0ae1c1b0c4fd776a2078e3c94160f17
|
|
@ -13969,7 +13969,8 @@ CREATE TABLE keys (
|
|||
last_used_at timestamp without time zone,
|
||||
fingerprint_sha256 bytea,
|
||||
expires_at timestamp with time zone,
|
||||
expiry_notification_delivered_at timestamp with time zone
|
||||
expiry_notification_delivered_at timestamp with time zone,
|
||||
before_expiry_notification_delivered_at timestamp with time zone
|
||||
);
|
||||
|
||||
CREATE SEQUENCE keys_id_seq
|
||||
|
@ -21729,6 +21730,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON jira_connect_s
|
|||
|
||||
CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id);
|
||||
|
||||
CREATE INDEX idx_keys_expires_at_and_before_expiry_notification_undelivered ON keys USING btree (date(timezone('UTC'::text, expires_at)), before_expiry_notification_delivered_at) WHERE (before_expiry_notification_delivered_at IS NULL);
|
||||
|
||||
CREATE INDEX idx_members_created_at_user_id_invite_token ON members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL));
|
||||
|
||||
CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4));
|
||||
|
@ -23161,6 +23164,8 @@ CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON n
|
|||
|
||||
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_namespaces_id_parent_id_is_null ON namespaces USING btree (id) WHERE (parent_id IS NULL);
|
||||
|
||||
CREATE INDEX index_namespaces_on_created_at ON namespaces USING btree (created_at);
|
||||
|
||||
CREATE INDEX index_namespaces_on_custom_project_templates_group_id_and_type ON namespaces USING btree (custom_project_templates_group_id, type) WHERE (custom_project_templates_group_id IS NOT NULL);
|
||||
|
|
|
@ -412,7 +412,7 @@ in the regression issue as fixes are addressed.
|
|||
|
||||
In order to track things that can be improved in the GitLab codebase,
|
||||
we use the ~"technical debt" label in the [GitLab issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues).
|
||||
For missed user experience requirements, we use the ~"UX debt" label.
|
||||
We use the ~"UX debt" label when we choose to deviate from the MVC, in a way that harms the user experience.
|
||||
|
||||
These labels should be added to issues that describe things that can be improved,
|
||||
shortcuts that have been taken, features that need additional attention, and all
|
||||
|
|
|
@ -9956,6 +9956,30 @@ Status: `implemented`
|
|||
|
||||
Tiers: `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.epics_usage.g_project_management_epic_users_changing_labels_monthly`
|
||||
|
||||
Count of MAU chaging the epic lables
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312195730_g_project_management_epic_labels_monthly.yml)
|
||||
|
||||
Group: `group::product planning`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.epics_usage.g_project_management_epic_users_changing_labels_weekly`
|
||||
|
||||
Count of WAU chaging the epic lables
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312195849_g_project_management_epic_labels_weekly.yml)
|
||||
|
||||
Group: `group::product planning`
|
||||
|
||||
Status: `implemented`
|
||||
|
||||
Tiers: `premium`, `ultimate`
|
||||
|
||||
### `redis_hll_counters.epics_usage.g_project_management_issue_promoted_to_epic_monthly`
|
||||
|
||||
Count of MAU promoting issues to epics
|
||||
|
|
|
@ -218,7 +218,8 @@ To use SSH with GitLab, copy your public key to your GitLab account.
|
|||
The expiration date is informational only, and does not prevent you from using
|
||||
the key. However, administrators can view expiration dates and
|
||||
use them for guidance when [deleting keys](../user/admin_area/credentials_inventory.md#delete-a-users-ssh-key).
|
||||
GitLab checks all SSH keys at 02:00 AM UTC every day. It emails an expiration notice for all SSH keys that expire on the current date. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.)
|
||||
- GitLab checks all SSH keys at 02:00 AM UTC every day. It emails an expiration notice for all SSH keys that expire on the current date. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.)
|
||||
- GitLab checks all SSH keys at 01:00 AM UTC every day. It emails an expiration notice for all SSH keys that are scheduled to expire seven days from now. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.)
|
||||
1. Select **Add key**.
|
||||
|
||||
## Verify that you can connect
|
||||
|
|
|
@ -11,6 +11,9 @@ GitLab provides a simple tool to administrators for emailing all users, or users
|
|||
a chosen group or project, right from the Admin Area. Users receive the email
|
||||
at their primary email address.
|
||||
|
||||
For information about email notifications originating from GitLab, read
|
||||
[GitLab notification emails](../user/profile/notifications.md).
|
||||
|
||||
## Use-cases
|
||||
|
||||
- Notify your users about a new project, a new feature, or a new product launch.
|
||||
|
|
|
@ -1143,6 +1143,22 @@ Profiles:
|
|||
UnicodeFuzzing: true
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Failed to start scanner session (version header not found)
|
||||
|
||||
The API Fuzzing engine outputs an error message when it cannot establish a connection with the scanner application component. The error message is shown in the job output window of the `apifuzzer_fuzz` job. A common cause of this issue is changing the `FUZZAPI_API` variable from its default.
|
||||
|
||||
**Error message**
|
||||
|
||||
- In [GitLab 13.11 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/323939), `Failed to start scanner session (version header not found).`
|
||||
- In GitLab 13.10 and earlier, `API Security version header not found. Are you sure that you are connecting to the API Security server?`.
|
||||
|
||||
**Solution**
|
||||
|
||||
- Remove the `FUZZAPI_API` variable from the `.gitlab-ci.yml` file. The value will be inherited from the API Fuzzing CI/CD template. We recommend this method instead of manually setting a value.
|
||||
- If removing the variable is not possible, check to see if this value has changed in the latest version of the [API Fuzzing CI/CD template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml). If so, update the value in the `.gitlab-ci.yml` file.
|
||||
|
||||
<!--
|
||||
### Target Container
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ GitLab Notifications allow you to stay informed about what's happening in GitLab
|
|||
enabled, you can receive updates about activity in issues, merge requests, epics, and designs.
|
||||
Notifications are sent via email.
|
||||
|
||||
For the tool that enables GitLab administrators to send messages to users, read
|
||||
[Email from GitLab](../../tools/email.md).
|
||||
|
||||
## Receiving notifications
|
||||
|
||||
You receive notifications for one of the following reasons:
|
||||
|
|
|
@ -122,3 +122,9 @@
|
|||
redis_slot: project_management
|
||||
aggregation: daily
|
||||
feature_flag: track_epics_activity
|
||||
|
||||
- name: g_project_management_epic_users_changing_labels
|
||||
category: epics_usage
|
||||
redis_slot: project_management
|
||||
aggregation: daily
|
||||
feature_flag: track_epics_activity
|
||||
|
|
|
@ -8948,6 +8948,9 @@ msgstr ""
|
|||
msgid "Create blank project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create blank project/repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create branch"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13805,6 +13808,9 @@ msgstr ""
|
|||
msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner."
|
||||
msgstr ""
|
||||
|
||||
msgid "Full"
|
||||
msgstr ""
|
||||
|
||||
msgid "Full name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14012,6 +14018,9 @@ msgstr ""
|
|||
msgid "Geo|%{name} is scheduled for re-verify"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|%{timeAgoStr} (%{pendingEvents} events)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|%{title} checksum progress"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14075,6 +14084,9 @@ msgstr ""
|
|||
msgid "Geo|Geo Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Geo nodes are paused using a command run on the node"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Geo sites"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14228,6 +14240,9 @@ msgstr ""
|
|||
msgid "Geo|Secondary site"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Selective (%{syncLabel})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Status"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15982,6 +15997,9 @@ msgstr ""
|
|||
msgid "Import project members"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import project/repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import projects from Bitbucket"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20920,6 +20938,9 @@ msgstr ""
|
|||
msgid "New project"
|
||||
msgstr ""
|
||||
|
||||
msgid "New project/repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "New release"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22580,6 +22601,9 @@ msgstr ""
|
|||
msgid "Pause replication"
|
||||
msgstr ""
|
||||
|
||||
msgid "Paused"
|
||||
msgstr ""
|
||||
|
||||
msgid "Paused runners don't accept new jobs"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24794,6 +24818,9 @@ msgstr ""
|
|||
msgid "ProjectsNew|Create blank project"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Create blank project/repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Create from template"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24812,6 +24839,9 @@ msgstr ""
|
|||
msgid "ProjectsNew|Import project"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Import project/repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectsNew|Initialize repository with a README"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25755,6 +25785,9 @@ msgstr ""
|
|||
msgid "Remediations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remember me"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remind later"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28542,6 +28575,9 @@ msgstr ""
|
|||
msgid "Sign in via 2FA code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sign in with"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sign in with Single Sign-On"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35610,12 +35646,18 @@ msgstr ""
|
|||
msgid "Your SSH key has expired"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your SSH key is expiring soon."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your SSH key was deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your SSH keys (%{count})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your SSH keys with the following fingerprints are scheduled to expire soon:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your SSH keys with the following fingerprints has expired:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
|||
module Dashboard
|
||||
module Snippet
|
||||
class Index < Page::Base
|
||||
view 'app/views/layouts/header/_new_dropdown.haml' do
|
||||
view 'app/views/layouts/header/_new_dropdown.html.haml' do
|
||||
element :new_menu_toggle
|
||||
element :global_new_snippet_link
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ module QA
|
|||
element :file_tree_table
|
||||
end
|
||||
|
||||
view 'app/views/layouts/header/_new_dropdown.haml' do
|
||||
view 'app/views/layouts/header/_new_dropdown.html.haml' do
|
||||
element :new_menu_toggle
|
||||
element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
|
|
@ -448,6 +448,12 @@ RSpec.describe ProjectsController do
|
|||
|
||||
post :create, params: { project: project_params }
|
||||
end
|
||||
|
||||
it 'tracks a created event for the new_repo experiment', :experiment do
|
||||
expect(experiment(:new_repo, :candidate)).to track(:project_created).on_next_instance
|
||||
|
||||
post :create, params: { project: project_params }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #archive' do
|
||||
|
|
|
@ -12,6 +12,72 @@ RSpec.describe 'New project', :js do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'new repo experiment', :experiment do
|
||||
it 'when in control renders "project"' do
|
||||
stub_experiments(new_repo: :control)
|
||||
|
||||
visit new_project_path
|
||||
|
||||
find('li.header-new.dropdown').click
|
||||
|
||||
page.within('li.header-new.dropdown') do
|
||||
expect(page).to have_selector('a', text: 'New project')
|
||||
expect(page).to have_no_selector('a', text: 'New project/repository')
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.blank-state-title', text: 'Create blank project')
|
||||
expect(page).to have_no_selector('.blank-state-title', text: 'Create blank project/repository')
|
||||
end
|
||||
|
||||
it 'when in candidate renders "project/repository"' do
|
||||
stub_experiments(new_repo: :candidate)
|
||||
|
||||
visit new_project_path
|
||||
|
||||
find('li.header-new.dropdown').click
|
||||
|
||||
page.within('li.header-new.dropdown') do
|
||||
expect(page).to have_selector('a', text: 'New project/repository')
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.blank-state-title', text: 'Create blank project/repository')
|
||||
end
|
||||
|
||||
context 'with combined_menu feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(combined_menu: false)
|
||||
end
|
||||
|
||||
it 'when in control it renders "project" in the new projects dropdown' do
|
||||
stub_experiments(new_repo: :control)
|
||||
|
||||
visit new_project_path
|
||||
|
||||
find('#nav-projects-dropdown').click
|
||||
|
||||
page.within('#nav-projects-dropdown') do
|
||||
expect(page).to have_selector('a', text: 'Create blank project')
|
||||
expect(page).to have_selector('a', text: 'Import project')
|
||||
expect(page).to have_no_selector('a', text: 'Create blank project/repository')
|
||||
expect(page).to have_no_selector('a', text: 'Import project/repository')
|
||||
end
|
||||
end
|
||||
|
||||
it 'when in candidate it renders "project/repository" in the new projects dropdown' do
|
||||
stub_experiments(new_repo: :candidate)
|
||||
|
||||
visit new_project_path
|
||||
|
||||
find('#nav-projects-dropdown').click
|
||||
|
||||
page.within('#nav-projects-dropdown') do
|
||||
expect(page).to have_selector('a', text: 'Create blank project/repository')
|
||||
expect(page).to have_selector('a', text: 'Import project/repository')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows a message if multiple levels are restricted' do
|
||||
Gitlab::CurrentSettings.update!(
|
||||
restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
|
||||
|
|
|
@ -116,6 +116,22 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with nested groups' do
|
||||
let(:subgroup) { create(:group, parent: group) }
|
||||
|
||||
context 'when user is not a top level group owner' do
|
||||
before do
|
||||
subgroup.add_owner(user)
|
||||
end
|
||||
|
||||
it 'does not show group settings link' do
|
||||
visit project_settings_access_tokens_path(project)
|
||||
|
||||
expect(page).not_to have_link('group settings', href: edit_group_path(group))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is a group owner' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
|
|
|
@ -1507,19 +1507,42 @@ describe('DiffsStoreActions', () => {
|
|||
});
|
||||
|
||||
describe('setFileByFile', () => {
|
||||
const updateUserEndpoint = 'user/prefs';
|
||||
let putSpy;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
putSpy = jest.spyOn(axios, 'put');
|
||||
|
||||
mock.onPut(updateUserEndpoint).reply(200, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it.each`
|
||||
value
|
||||
${true}
|
||||
${false}
|
||||
`('commits SET_FILE_BY_FILE with the new value $value', ({ value }) => {
|
||||
return testAction(
|
||||
setFileByFile,
|
||||
{ fileByFile: value },
|
||||
{ viewDiffsFileByFile: null },
|
||||
[{ type: types.SET_FILE_BY_FILE, payload: value }],
|
||||
[],
|
||||
);
|
||||
});
|
||||
`(
|
||||
'commits SET_FILE_BY_FILE and persists the File-by-File user preference with the new value $value',
|
||||
async ({ value }) => {
|
||||
await testAction(
|
||||
setFileByFile,
|
||||
{ fileByFile: value },
|
||||
{
|
||||
viewDiffsFileByFile: null,
|
||||
endpointUpdateUser: updateUserEndpoint,
|
||||
},
|
||||
[{ type: types.SET_FILE_BY_FILE, payload: value }],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(putSpy).toHaveBeenCalledWith(updateUserEndpoint, { view_diffs_file_by_file: value });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('reviewFile', () => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlBreadcrumb } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
|
||||
import App from '~/projects/experiment_new_project_creation/components/app.vue';
|
||||
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
|
||||
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
|
||||
|
@ -17,6 +18,34 @@ describe('Experimental new project creation app', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
|
||||
const findPanel = (panelName) =>
|
||||
findWelcomePage()
|
||||
.props()
|
||||
.panels.find((p) => p.name === panelName);
|
||||
|
||||
describe('new_repo experiment', () => {
|
||||
describe('when in the candidate variant', () => {
|
||||
assignGitlabExperiment('new_repo', 'candidate');
|
||||
|
||||
it('has "repository" in the panel title', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findPanel('blank_project').title).toBe('Create blank project/repository');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when in the control variant', () => {
|
||||
assignGitlabExperiment('new_repo', 'control');
|
||||
|
||||
it('has "project" in the panel title', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findPanel('blank_project').title).toBe('Create blank project');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with empty hash', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
|
||||
import { getExperimentData } from '~/experimentation/utils';
|
||||
import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue';
|
||||
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
|
||||
|
||||
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
|
||||
|
||||
describe('Welcome page', () => {
|
||||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
@ -14,6 +19,7 @@ describe('Welcome page', () => {
|
|||
beforeEach(() => {
|
||||
trackingSpy = mockTracking('_category_', document, jest.spyOn);
|
||||
trackingSpy.mockImplementation(() => {});
|
||||
getExperimentData.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -22,14 +28,35 @@ describe('Welcome page', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
it('tracks link clicks', () => {
|
||||
it('tracks link clicks', async () => {
|
||||
createComponent({ panels: [{ name: 'test', href: '#' }] });
|
||||
wrapper.find('a').trigger('click');
|
||||
const link = wrapper.find('a');
|
||||
link.trigger('click');
|
||||
await nextTick();
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
it('adds new_repo experiment data if in experiment', async () => {
|
||||
const mockExperimentData = 'data';
|
||||
getExperimentData.mockReturnValue(mockExperimentData);
|
||||
|
||||
createComponent({ panels: [{ name: 'test', href: '#' }] });
|
||||
const link = wrapper.find('a');
|
||||
link.trigger('click');
|
||||
await nextTick();
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', {
|
||||
label: 'test',
|
||||
context: {
|
||||
data: mockExperimentData,
|
||||
schema: TRACKING_CONTEXT_SCHEMA,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders new project push tip popover', () => {
|
||||
createComponent({ panels: [{ name: 'test', href: '#' }] });
|
||||
|
||||
|
|
|
@ -212,52 +212,101 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'notification email for expired ssh key' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
describe 'SSH key notification' do
|
||||
let_it_be_with_reload(:user) { create(:user) }
|
||||
let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] }
|
||||
|
||||
context 'when valid' do
|
||||
subject { Notify.ssh_key_expired_email(user, fingerprints) }
|
||||
shared_examples 'is sent to the user' do
|
||||
it { is_expected.to deliver_to user.email }
|
||||
end
|
||||
|
||||
shared_examples 'has the correct subject' do |subject_text|
|
||||
it { is_expected.to have_subject subject_text }
|
||||
end
|
||||
|
||||
shared_examples 'has the correct body text' do |body_text|
|
||||
it { is_expected.to have_body_text body_text }
|
||||
end
|
||||
|
||||
shared_examples 'includes a link to ssh key page' do
|
||||
it { is_expected.to have_body_text /#{profile_keys_url}/ }
|
||||
end
|
||||
|
||||
shared_examples 'includes the email reason' do
|
||||
it { is_expected.to have_body_text /You're receiving this email because of your account on localhost/ }
|
||||
end
|
||||
|
||||
shared_examples 'valid use case' do
|
||||
it_behaves_like 'an email sent from GitLab'
|
||||
it_behaves_like 'it should not have Gmail Actions links'
|
||||
it_behaves_like 'a user cannot unsubscribe through footer link'
|
||||
it_behaves_like 'is sent to the user'
|
||||
it_behaves_like 'includes a link to ssh key page'
|
||||
it_behaves_like 'includes the email reason'
|
||||
end
|
||||
|
||||
it 'is sent to the user' do
|
||||
is_expected.to deliver_to user.email
|
||||
end
|
||||
|
||||
it 'has the correct subject' do
|
||||
is_expected.to have_subject /Your SSH key has expired/
|
||||
end
|
||||
|
||||
it 'mentions the ssh keu has expired' do
|
||||
is_expected.to have_body_text /Your SSH keys with the following fingerprints has expired/
|
||||
end
|
||||
|
||||
it 'includes a link to ssh key page' do
|
||||
is_expected.to have_body_text /#{profile_keys_url}/
|
||||
end
|
||||
|
||||
it 'includes the email reason' do
|
||||
is_expected.to have_body_text /You're receiving this email because of your account on localhost/
|
||||
shared_examples 'does not send email' do
|
||||
it do
|
||||
expect { subject }.not_to change { ActionMailer::Base.deliveries.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid' do
|
||||
context 'when user does not exist' do
|
||||
it do
|
||||
expect { Notify.ssh_key_expired_email(nil) }.not_to change { ActionMailer::Base.deliveries.count }
|
||||
end
|
||||
shared_context 'block user' do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
end
|
||||
|
||||
context 'notification email for expired ssh key' do
|
||||
context 'when valid' do
|
||||
subject { Notify.ssh_key_expired_email(user, fingerprints) }
|
||||
|
||||
include_examples 'valid use case'
|
||||
|
||||
it_behaves_like 'has the correct subject', /Your SSH key has expired/
|
||||
it_behaves_like 'has the correct body text', /Your SSH keys with the following fingerprints has expired/
|
||||
end
|
||||
|
||||
context 'when user is not active' do
|
||||
before do
|
||||
user.block!
|
||||
context 'when invalid' do
|
||||
context 'when user does not exist' do
|
||||
subject { Notify.ssh_key_expired_email(nil, fingerprints) }
|
||||
|
||||
it_behaves_like 'does not send email'
|
||||
end
|
||||
|
||||
it do
|
||||
expect { Notify.ssh_key_expired_email(user) }.not_to change { ActionMailer::Base.deliveries.count }
|
||||
context 'when user is not active' do
|
||||
subject { Notify.ssh_key_expired_email(user, fingerprints) }
|
||||
|
||||
include_context 'block user'
|
||||
|
||||
it_behaves_like 'does not send email'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'notification email for expiring ssh key' do
|
||||
context 'when valid' do
|
||||
subject { Notify.ssh_key_expiring_soon_email(user, fingerprints) }
|
||||
|
||||
include_examples 'valid use case'
|
||||
|
||||
it_behaves_like 'has the correct subject', /Your SSH key is expiring soon/
|
||||
it_behaves_like 'has the correct body text', /Your SSH keys with the following fingerprints are scheduled to expire soon/
|
||||
end
|
||||
|
||||
context 'when invalid' do
|
||||
context 'when user does not exist' do
|
||||
subject { Notify.ssh_key_expiring_soon_email(nil, fingerprints) }
|
||||
|
||||
it_behaves_like 'does not send email'
|
||||
end
|
||||
|
||||
context 'when user is not active' do
|
||||
subject { Notify.ssh_key_expiring_soon_email(user, fingerprints) }
|
||||
|
||||
include_context 'block user'
|
||||
|
||||
it_behaves_like 'does not send email'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -76,15 +76,25 @@ RSpec.describe Key, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.expired_today_and_not_notified' do
|
||||
context 'expiration scopes' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user) }
|
||||
let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user, expiry_notification_delivered_at: Time.current) }
|
||||
let_it_be(:expired_yesterday) { create(:key, expires_at: 1.day.ago, user: user) }
|
||||
let_it_be(:future_expiry) { create(:key, expires_at: 1.day.from_now, user: user) }
|
||||
let_it_be(:expiring_soon_unotified) { create(:key, expires_at: 3.days.from_now, user: user) }
|
||||
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 4.days.from_now, user: user, before_expiry_notification_delivered_at: Time.current) }
|
||||
let_it_be(:future_expiry) { create(:key, expires_at: 1.month.from_now, user: user) }
|
||||
|
||||
it 'returns tokens that have expired today' do
|
||||
expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified)
|
||||
describe '.expired_today_and_not_notified' do
|
||||
it 'returns keys that expire today' do
|
||||
expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.expiring_soon_and_not_notified' do
|
||||
it 'returns keys that will expire soon' do
|
||||
expect(described_class.expiring_soon_and_not_notified).to contain_exactly(expiring_soon_unotified)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1001,15 +1001,24 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.with_ssh_key_expired_today' do
|
||||
context 'SSH key expiration scopes' do
|
||||
let_it_be(:user1) { create(:user) }
|
||||
let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user1) }
|
||||
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user1) }
|
||||
let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user2, expiry_notification_delivered_at: Time.current) }
|
||||
let_it_be(:expiring_soon_not_notified) { create(:key, expires_at: 2.days.from_now, user: user2) }
|
||||
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 2.days.from_now, user: user1, before_expiry_notification_delivered_at: Time.current) }
|
||||
|
||||
it 'returns users whose token has expired today' do
|
||||
expect(described_class.with_ssh_key_expired_today).to contain_exactly(user1)
|
||||
describe '.with_ssh_key_expired_today' do
|
||||
it 'returns users whose key has expired today' do
|
||||
expect(described_class.with_ssh_key_expired_today).to contain_exactly(user1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.with_ssh_key_expiring_soon' do
|
||||
it 'returns users whose keys will expire soon' do
|
||||
expect(described_class.with_ssh_key_expiring_soon).to contain_exactly(user2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -313,4 +313,38 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ci related paths' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:path_field, :method_for_existence_check) do
|
||||
:terraform_reports_path | :has_terraform_reports?
|
||||
:accessibility_report_path | :has_accessibility_reports?
|
||||
:exposed_artifacts_path | :has_exposed_artifacts?
|
||||
:test_reports_path | :has_test_reports?
|
||||
:codequality_reports_path | :has_codequality_reports?
|
||||
end
|
||||
|
||||
with_them do
|
||||
context 'when merge request has reports' do
|
||||
before do
|
||||
allow(resource).to receive(method_for_existence_check).and_return(true)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[path_field]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request has no reports' do
|
||||
before do
|
||||
allow(resource).to receive(method_for_existence_check).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not set reports path' do
|
||||
expect(subject[path_field]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,72 +87,6 @@ RSpec.describe MergeRequestPollWidgetEntity do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'terraform_reports_path' do
|
||||
context 'when merge request has terraform reports' do
|
||||
before do
|
||||
allow(resource).to receive(:has_terraform_reports?).and_return(true)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[:terraform_reports_path]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request has no terraform reports' do
|
||||
before do
|
||||
allow(resource).to receive(:has_terraform_reports?).and_return(false)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[:terraform_reports_path]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'accessibility_report_path' do
|
||||
context 'when merge request has accessibility reports' do
|
||||
before do
|
||||
allow(resource).to receive(:has_accessibility_reports?).and_return(true)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[:accessibility_report_path]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request has no accessibility reports' do
|
||||
before do
|
||||
allow(resource).to receive(:has_accessibility_reports?).and_return(false)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[:accessibility_report_path]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'exposed_artifacts_path' do
|
||||
context 'when merge request has exposed artifacts' do
|
||||
before do
|
||||
expect(resource).to receive(:has_exposed_artifacts?).and_return(true)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[:exposed_artifacts_path]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request has no exposed artifacts' do
|
||||
before do
|
||||
expect(resource).to receive(:has_exposed_artifacts?).and_return(false)
|
||||
end
|
||||
|
||||
it 'set the path to poll data' do
|
||||
expect(subject[:exposed_artifacts_path]).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'auto merge' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
|
|
@ -4,48 +4,93 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Keys::ExpiryNotificationService do
|
||||
let_it_be_with_reload(:user) { create(:user) }
|
||||
let_it_be_with_reload(:expired_key) { create(:key, expires_at: Time.current, user: user) }
|
||||
|
||||
let(:params) { { keys: keys } }
|
||||
let(:params) { { keys: user.keys, expiring_soon: expiring_soon } }
|
||||
|
||||
subject { described_class.new(user, params) }
|
||||
|
||||
context 'with expired key', :mailer do
|
||||
let(:keys) { user.keys }
|
||||
|
||||
it 'sends a notification' do
|
||||
shared_examples 'sends a notification' do
|
||||
it do
|
||||
perform_enqueued_jobs do
|
||||
subject.execute
|
||||
end
|
||||
should_email(user)
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses notification service to send email to the user' do
|
||||
shared_examples 'uses notification service to send email to the user' do |notification_method|
|
||||
it do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
expect(notification_service).to receive(:ssh_key_expired).with(expired_key.user, [expired_key.fingerprint])
|
||||
expect(notification_service).to receive(notification_method).with(key.user, [key.fingerprint])
|
||||
end
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates notified column' do
|
||||
expect { subject.execute }.to change { expired_key.reload.expiry_notification_delivered_at }
|
||||
shared_examples 'does not send notification' do
|
||||
it do
|
||||
perform_enqueued_jobs do
|
||||
subject.execute
|
||||
end
|
||||
should_not_email(user)
|
||||
end
|
||||
end
|
||||
|
||||
shared_context 'block user' do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
end
|
||||
|
||||
context 'with key expiring today', :mailer do
|
||||
let_it_be_with_reload(:key) { create(:key, expires_at: Time.current, user: user) }
|
||||
|
||||
let(:expiring_soon) { false }
|
||||
|
||||
context 'when user has permission to receive notification' do
|
||||
it_behaves_like 'sends a notification'
|
||||
|
||||
it_behaves_like 'uses notification service to send email to the user', :ssh_key_expired
|
||||
|
||||
it 'updates notified column' do
|
||||
expect { subject.execute }.to change { key.reload.expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have permission to receive notification' do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
context 'when user does NOT have permission to receive notification' do
|
||||
include_context 'block user'
|
||||
|
||||
it 'does not send notification' do
|
||||
perform_enqueued_jobs do
|
||||
subject.execute
|
||||
end
|
||||
should_not_email(user)
|
||||
end
|
||||
it_behaves_like 'does not send notification'
|
||||
|
||||
it 'does not update notified column' do
|
||||
expect { subject.execute }.not_to change { expired_key.reload.expiry_notification_delivered_at }
|
||||
expect { subject.execute }.not_to change { key.reload.expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with key expiring soon', :mailer do
|
||||
let_it_be_with_reload(:key) { create(:key, expires_at: 3.days.from_now, user: user) }
|
||||
|
||||
let(:expiring_soon) { true }
|
||||
|
||||
context 'when user has permission to receive notification' do
|
||||
it_behaves_like 'sends a notification'
|
||||
|
||||
it_behaves_like 'uses notification service to send email to the user', :ssh_key_expiring_soon
|
||||
|
||||
it 'updates notified column' do
|
||||
expect { subject.execute }.to change { key.reload.before_expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does NOT have permission to receive notification' do
|
||||
include_context 'block user'
|
||||
|
||||
it_behaves_like 'does not send notification'
|
||||
|
||||
it 'does not update notified column' do
|
||||
expect { subject.execute }.not_to change { key.reload.before_expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -288,11 +288,19 @@ RSpec.describe NotificationService, :mailer do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'SSH Keys' do
|
||||
let_it_be_with_reload(:user) { create(:user) }
|
||||
let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] }
|
||||
|
||||
shared_context 'block user' do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ssh_key_expired' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] }
|
||||
|
||||
subject { notification.ssh_key_expired(user, fingerprints) }
|
||||
|
||||
it 'sends email to the token owner' do
|
||||
|
@ -300,15 +308,29 @@ RSpec.describe NotificationService, :mailer do
|
|||
end
|
||||
|
||||
context 'when user is not allowed to receive notifications' do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
include_context 'block user'
|
||||
|
||||
it 'does not send email to the token owner' do
|
||||
expect { subject }.not_to have_enqueued_email(user, fingerprints, mail: "ssh_key_expired_email")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ssh_key_expiring_soon' do
|
||||
subject { notification.ssh_key_expiring_soon(user, fingerprints) }
|
||||
|
||||
it 'sends email to the token owner' do
|
||||
expect { subject }.to have_enqueued_email(user, fingerprints, mail: "ssh_key_expiring_soon_email")
|
||||
end
|
||||
|
||||
context 'when user is not allowed to receive notifications' do
|
||||
include_context 'block user'
|
||||
|
||||
it 'does not send email to the token owner' do
|
||||
expect { subject }.not_to have_enqueued_email(user, fingerprints, mail: "ssh_key_expiring_soon_email")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unknown_sign_in' do
|
||||
|
|
|
@ -163,6 +163,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
|
|||
end
|
||||
|
||||
it 'has a "New project" link' do
|
||||
render('layouts/header/new_repo_experiment')
|
||||
render
|
||||
|
||||
expect(rendered).to have_link('New project', href: new_project_path)
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do
|
||||
subject(:worker) { described_class.new }
|
||||
|
||||
it 'uses a cronjob queue' do
|
||||
expect(worker.sidekiq_options_hash).to include(
|
||||
'queue' => 'cronjob:ssh_keys_expiring_soon_notification',
|
||||
'queue_namespace' => :cronjob
|
||||
)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
context 'with key expiring soon' do
|
||||
let_it_be_with_reload(:expiring_soon) { create(:key, expires_at: 6.days.from_now, user: user) }
|
||||
|
||||
it 'invoke the notification service' do
|
||||
expect_next_instance_of(Keys::ExpiryNotificationService) do |expiry_service|
|
||||
expect(expiry_service).to receive(:execute)
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
|
||||
it 'updates notified column' do
|
||||
expect { worker.perform }.to change { expiring_soon.reload.before_expiry_notification_delivered_at }
|
||||
end
|
||||
|
||||
include_examples 'an idempotent worker' do
|
||||
subject do
|
||||
perform_multiple(worker: worker)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(ssh_key_expiration_email_notification: false)
|
||||
end
|
||||
|
||||
it 'does not update notified column' do
|
||||
expect { worker.perform }.not_to change { expiring_soon.reload.before_expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when key has expired in the past' do
|
||||
let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) }
|
||||
|
||||
it 'does not update notified column' do
|
||||
expect { worker.perform }.not_to change { expired_past.reload.before_expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when key is not expiring soon' do
|
||||
let_it_be(:expires_future) { create(:key, expires_at: 8.days.from_now, user: user) }
|
||||
|
||||
it 'does not update notified column' do
|
||||
expect { worker.perform }.not_to change { expires_future.reload.before_expiry_notification_delivered_at }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue