Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e9112f3002
commit
1aa9cd3080
39 changed files with 1706 additions and 45 deletions
|
@ -40,7 +40,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
||||||
project_pipelines_url(group.projects.first)
|
project_pipelines_url(group.projects.first)
|
||||||
when :trial, :trial_short
|
when :trial, :trial_short
|
||||||
'https://about.gitlab.com/free-trial/'
|
'https://about.gitlab.com/free-trial/'
|
||||||
when :team, :team_short
|
when :team, :team_short, :invite_team
|
||||||
group_group_members_url(group)
|
group_group_members_url(group)
|
||||||
when :admin_verify
|
when :admin_verify
|
||||||
project_settings_ci_cd_path(group.projects.first, ci_runner_templates: true, anchor: 'js-runners-settings')
|
project_settings_ci_cd_path(group.projects.first, ci_runner_templates: true, anchor: 'js-runners-settings')
|
||||||
|
@ -59,6 +59,11 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
|
||||||
@track = params[:track]&.to_sym
|
@track = params[:track]&.to_sym
|
||||||
@series = params[:series]&.to_i
|
@series = params[:series]&.to_i
|
||||||
|
|
||||||
|
# There is only one email that will be sent for invite team track so series
|
||||||
|
# should only have the value 0. Return early if track is invite team and
|
||||||
|
# condition for series value is met
|
||||||
|
return if @track == Namespaces::InviteTeamEmailService::TRACK && @series == 0
|
||||||
|
|
||||||
track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
|
track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
|
||||||
return render_404 unless track_valid
|
return render_404 unless track_valid
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,8 @@ module Users
|
||||||
experience: 4,
|
experience: 4,
|
||||||
team_short: 5,
|
team_short: 5,
|
||||||
trial_short: 6,
|
trial_short: 6,
|
||||||
admin_verify: 7
|
admin_verify: 7,
|
||||||
|
invite_team: 8
|
||||||
}, _suffix: true
|
}, _suffix: true
|
||||||
|
|
||||||
scope :without_track_and_series, -> (track, series) do
|
scope :without_track_and_series, -> (track, series) do
|
||||||
|
|
|
@ -57,7 +57,10 @@ module Groups
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_create_hook
|
def after_create_hook
|
||||||
# overridden in EE
|
if group.persisted? && group.root?
|
||||||
|
delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
|
||||||
|
Namespaces::InviteTeamEmailWorker.perform_in(delay, group.id, current_user.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_unallowed_params
|
def remove_unallowed_params
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Namespaces
|
||||||
|
class InProductMarketingEmailRecords
|
||||||
|
attr_reader :records
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@records = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def save!
|
||||||
|
Users::InProductMarketingEmail.bulk_insert!(@records)
|
||||||
|
@records = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def add(user, track, series)
|
||||||
|
@records << Users::InProductMarketingEmail.new(
|
||||||
|
user: user,
|
||||||
|
track: track,
|
||||||
|
series: series,
|
||||||
|
created_at: Time.zone.now,
|
||||||
|
updated_at: Time.zone.now
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -56,7 +56,7 @@ module Namespaces
|
||||||
def initialize(track, interval)
|
def initialize(track, interval)
|
||||||
@track = track
|
@track = track
|
||||||
@interval = interval
|
@interval = interval
|
||||||
@in_product_marketing_email_records = []
|
@sent_email_records = InProductMarketingEmailRecords.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
|
@ -71,17 +71,21 @@ module Namespaces
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :track, :interval, :in_product_marketing_email_records
|
attr_reader :track, :interval, :sent_email_records
|
||||||
|
|
||||||
|
def send_email(user, group)
|
||||||
|
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
|
||||||
|
end
|
||||||
|
|
||||||
def send_email_for_group(group)
|
def send_email_for_group(group)
|
||||||
users_for_group(group).each do |user|
|
users_for_group(group).each do |user|
|
||||||
if can_perform_action?(user, group)
|
if can_perform_action?(user, group)
|
||||||
send_email(user, group)
|
send_email(user, group)
|
||||||
track_sent_email(user, track, series)
|
sent_email_records.add(user, track, series)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
save_tracked_emails!
|
sent_email_records.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def groups_for_track
|
def groups_for_track
|
||||||
|
@ -126,10 +130,6 @@ module Namespaces
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_email(user, group)
|
|
||||||
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
|
|
||||||
end
|
|
||||||
|
|
||||||
def completed_actions
|
def completed_actions
|
||||||
TRACKS[track][:completed_actions]
|
TRACKS[track][:completed_actions]
|
||||||
end
|
end
|
||||||
|
@ -146,21 +146,6 @@ module Namespaces
|
||||||
def series
|
def series
|
||||||
TRACKS[track][:interval_days].index(interval)
|
TRACKS[track][:interval_days].index(interval)
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_tracked_emails!
|
|
||||||
Users::InProductMarketingEmail.bulk_insert!(in_product_marketing_email_records)
|
|
||||||
@in_product_marketing_email_records = []
|
|
||||||
end
|
|
||||||
|
|
||||||
def track_sent_email(user, track, series)
|
|
||||||
in_product_marketing_email_records << Users::InProductMarketingEmail.new(
|
|
||||||
user: user,
|
|
||||||
track: track,
|
|
||||||
series: series,
|
|
||||||
created_at: Time.zone.now,
|
|
||||||
updated_at: Time.zone.now
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
62
app/services/namespaces/invite_team_email_service.rb
Normal file
62
app/services/namespaces/invite_team_email_service.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Namespaces
|
||||||
|
class InviteTeamEmailService
|
||||||
|
include Gitlab::Experiment::Dsl
|
||||||
|
|
||||||
|
TRACK = :invite_team
|
||||||
|
DELIVERY_DELAY_IN_MINUTES = 20.minutes
|
||||||
|
|
||||||
|
def self.send_email(user, group)
|
||||||
|
new(user, group).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(user, group)
|
||||||
|
@group = group
|
||||||
|
@user = user
|
||||||
|
@sent_email_records = InProductMarketingEmailRecords.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return unless user.email_opted_in?
|
||||||
|
return unless group.root?
|
||||||
|
return unless group.setup_for_company
|
||||||
|
|
||||||
|
# Exclude group if users other than the creator have already been
|
||||||
|
# added/invited
|
||||||
|
return unless group.member_count == 1
|
||||||
|
|
||||||
|
return if email_for_track_sent_to_user?
|
||||||
|
|
||||||
|
experiment(:invite_team_email, group: group) do |e|
|
||||||
|
e.candidate do
|
||||||
|
send_email(user, group)
|
||||||
|
sent_email_records.add(user, track, series)
|
||||||
|
sent_email_records.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
e.record!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :user, :group, :sent_email_records
|
||||||
|
|
||||||
|
def send_email(user, group)
|
||||||
|
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
|
||||||
|
end
|
||||||
|
|
||||||
|
def track
|
||||||
|
TRACK
|
||||||
|
end
|
||||||
|
|
||||||
|
def series
|
||||||
|
0
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_for_track_sent_to_user?
|
||||||
|
Users::InProductMarketingEmail.for_user_with_track_and_series(user, track, series).present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2411,6 +2411,15 @@
|
||||||
:weight: 1
|
:weight: 1
|
||||||
:idempotent:
|
:idempotent:
|
||||||
:tags: []
|
:tags: []
|
||||||
|
- :name: namespaces_invite_team_email
|
||||||
|
:worker_name: Namespaces::InviteTeamEmailWorker
|
||||||
|
:feature_category: :experimentation_activation
|
||||||
|
:has_external_dependencies:
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent:
|
||||||
|
:tags: []
|
||||||
- :name: namespaces_onboarding_issue_created
|
- :name: namespaces_onboarding_issue_created
|
||||||
:worker_name: Namespaces::OnboardingIssueCreatedWorker
|
:worker_name: Namespaces::OnboardingIssueCreatedWorker
|
||||||
:feature_category: :onboarding
|
:feature_category: :onboarding
|
||||||
|
|
22
app/workers/namespaces/invite_team_email_worker.rb
Normal file
22
app/workers/namespaces/invite_team_email_worker.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Namespaces
|
||||||
|
class InviteTeamEmailWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
|
||||||
|
data_consistency :always
|
||||||
|
|
||||||
|
feature_category :experimentation_activation
|
||||||
|
urgency :low
|
||||||
|
|
||||||
|
def perform(group_id, user_id)
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
user = User.find_by(id: user_id)
|
||||||
|
group = Group.find_by(id: group_id)
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
return unless user && group
|
||||||
|
|
||||||
|
Namespaces::InviteTeamEmailService.send_email(user, group)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340388
|
||||||
milestone: '14.3'
|
milestone: '14.3'
|
||||||
type: development
|
type: development
|
||||||
group: group::dynamic analysis
|
group: group::dynamic analysis
|
||||||
default_enabled: false
|
default_enabled: true
|
||||||
|
|
8
config/feature_flags/experiment/invite_team_email.yml
Normal file
8
config/feature_flags/experiment/invite_team_email.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: invite_team_email
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72470
|
||||||
|
rollout_issue_url:
|
||||||
|
milestone: '14.5'
|
||||||
|
type: experiment
|
||||||
|
group: group::activation
|
||||||
|
default_enabled: false
|
|
@ -255,6 +255,8 @@
|
||||||
- 1
|
- 1
|
||||||
- - namespaceless_project_destroy
|
- - namespaceless_project_destroy
|
||||||
- 1
|
- 1
|
||||||
|
- - namespaces_invite_team_email
|
||||||
|
- 1
|
||||||
- - namespaces_onboarding_issue_created
|
- - namespaces_onboarding_issue_created
|
||||||
- 1
|
- 1
|
||||||
- - namespaces_onboarding_pipeline_created
|
- - namespaces_onboarding_pipeline_created
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
response = ::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute
|
response = Sidekiq::Worker.skipping_transaction_check do
|
||||||
|
::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute
|
||||||
|
end
|
||||||
|
|
||||||
if response[:status] == :success
|
if response[:status] == :success
|
||||||
puts "Successfully created self monitoring project."
|
puts "Successfully created self monitoring project."
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexesToIssueStageEvents < Gitlab::Database::Migration[1.0]
|
||||||
|
include Gitlab::Database::PartitioningMigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
GROUP_INDEX_NAME = 'index_issue_stage_events_group_duration'
|
||||||
|
GROUP_IN_PROGRESS_INDEX_NAME = 'index_issue_stage_events_group_in_progress_duration'
|
||||||
|
PROJECT_INDEX_NAME = 'index_issue_stage_events_project_duration'
|
||||||
|
PROJECT_IN_PROGRESS_INDEX_NAME = 'index_issue_stage_events_project_in_progress_duration'
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||||
|
'stage_event_hash_id, group_id, end_event_timestamp, issue_id, start_event_timestamp',
|
||||||
|
where: 'end_event_timestamp IS NOT NULL',
|
||||||
|
name: GROUP_INDEX_NAME
|
||||||
|
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||||
|
'stage_event_hash_id, project_id, end_event_timestamp, issue_id, start_event_timestamp',
|
||||||
|
where: 'end_event_timestamp IS NOT NULL',
|
||||||
|
name: PROJECT_INDEX_NAME
|
||||||
|
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||||
|
'stage_event_hash_id, group_id, start_event_timestamp, issue_id',
|
||||||
|
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||||
|
name: GROUP_IN_PROGRESS_INDEX_NAME
|
||||||
|
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_issue_stage_events,
|
||||||
|
'stage_event_hash_id, project_id, start_event_timestamp, issue_id',
|
||||||
|
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||||
|
name: PROJECT_IN_PROGRESS_INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, GROUP_INDEX_NAME
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, PROJECT_INDEX_NAME
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, GROUP_IN_PROGRESS_INDEX_NAME
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_issue_stage_events, PROJECT_IN_PROGRESS_INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexesToMergeRequestStageEvents < Gitlab::Database::Migration[1.0]
|
||||||
|
include Gitlab::Database::PartitioningMigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
GROUP_INDEX_NAME = 'index_merge_request_stage_events_group_duration'
|
||||||
|
GROUP_IN_PROGRESS_INDEX_NAME = 'index_merge_request_stage_events_group_in_progress_duration'
|
||||||
|
PROJECT_INDEX_NAME = 'index_merge_request_stage_events_project_duration'
|
||||||
|
PROJECT_IN_PROGRESS_INDEX_NAME = 'index_merge_request_stage_events_project_in_progress_duration'
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||||
|
'stage_event_hash_id, group_id, end_event_timestamp, merge_request_id, start_event_timestamp',
|
||||||
|
where: 'end_event_timestamp IS NOT NULL',
|
||||||
|
name: GROUP_INDEX_NAME
|
||||||
|
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||||
|
'stage_event_hash_id, project_id, end_event_timestamp, merge_request_id, start_event_timestamp',
|
||||||
|
where: 'end_event_timestamp IS NOT NULL',
|
||||||
|
name: PROJECT_INDEX_NAME
|
||||||
|
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||||
|
'stage_event_hash_id, group_id, start_event_timestamp, merge_request_id',
|
||||||
|
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||||
|
name: GROUP_IN_PROGRESS_INDEX_NAME
|
||||||
|
|
||||||
|
add_concurrent_partitioned_index :analytics_cycle_analytics_merge_request_stage_events,
|
||||||
|
'stage_event_hash_id, project_id, start_event_timestamp, merge_request_id',
|
||||||
|
where: 'end_event_timestamp IS NULL AND state_id = 1',
|
||||||
|
name: PROJECT_IN_PROGRESS_INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, GROUP_INDEX_NAME
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, PROJECT_INDEX_NAME
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, GROUP_IN_PROGRESS_INDEX_NAME
|
||||||
|
remove_concurrent_partitioned_index_by_name :analytics_cycle_analytics_merge_request_stage_events, PROJECT_IN_PROGRESS_INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
1
db/schema_migrations/20211031152417
Normal file
1
db/schema_migrations/20211031152417
Normal file
|
@ -0,0 +1 @@
|
||||||
|
f46a0dd662a80d38a4e8d3e6c4db05e61563a959b75d30a4c3724ae6afc2647f
|
1
db/schema_migrations/20211031154919
Normal file
1
db/schema_migrations/20211031154919
Normal file
|
@ -0,0 +1 @@
|
||||||
|
08399bfbf62533c00dfe3ca3434f6be292ec768f053d3b1fde41d2d68de32fe7
|
1040
db/structure.sql
1040
db/structure.sql
File diff suppressed because it is too large
Load diff
|
@ -3864,15 +3864,11 @@ you can ensure that concurrent deployments never happen to the production enviro
|
||||||
|
|
||||||
Use `release` to create a [release](../../user/project/releases/index.md).
|
Use `release` to create a [release](../../user/project/releases/index.md).
|
||||||
|
|
||||||
The release job must have access to the [`release-cli`](https://gitlab.com/gitlab-org/release-cli/-/tree/master/docs)
|
The release job must have access to the [`release-cli`](https://gitlab.com/gitlab-org/release-cli/-/tree/master/docs),
|
||||||
and your runner must be using one of these executors:
|
which must be in the `$PATH`.
|
||||||
|
|
||||||
- If you use the [Docker executor](https://docs.gitlab.com/runner/executors/docker.html),
|
If you use the [Docker executor](https://docs.gitlab.com/runner/executors/docker.html),
|
||||||
your [`image:`](#image) must include the `release-cli`. You can use this image from the GitLab
|
you can use this image from the GitLab Container Registry: `registry.gitlab.com/gitlab-org/release-cli:latest`
|
||||||
Container Registry: `registry.gitlab.com/gitlab-org/release-cli:latest`
|
|
||||||
|
|
||||||
- If you use the [Shell executor](https://docs.gitlab.com/runner/executors/shell.html), the server
|
|
||||||
where the runner is registered must have the `release-cli` [installed](../../user/project/releases/release_cli.md).
|
|
||||||
|
|
||||||
**Keyword type**: Job keyword. You can use it only as part of a job.
|
**Keyword type**: Job keyword. You can use it only as part of a job.
|
||||||
|
|
||||||
|
@ -3921,6 +3917,8 @@ This example creates a release:
|
||||||
- The `release` section executes after the `script` keyword and before the `after_script`.
|
- The `release` section executes after the `script` keyword and before the `after_script`.
|
||||||
- A release is created only if the job's main script succeeds.
|
- A release is created only if the job's main script succeeds.
|
||||||
- If the release already exists, it is not updated and the job with the `release` keyword fails.
|
- If the release already exists, it is not updated and the job with the `release` keyword fails.
|
||||||
|
- If you use the [Shell executor](https://docs.gitlab.com/runner/executors/shell.html) or similar,
|
||||||
|
[install `release-cli`](../../user/project/releases/release_cli.md) on the server where the runner is registered.
|
||||||
|
|
||||||
**Related topics**:
|
**Related topics**:
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,7 @@ For example, [a pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/
|
||||||
[`pages` job](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/1733948332).
|
[`pages` job](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/1733948332).
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph LR
|
||||||
A{{"Container registry on gitlab-docs project"}}
|
A{{"Container registry on gitlab-docs project"}}
|
||||||
B[["Scheduled pipeline<br>`pages` and<br>`pages:deploy` job"]]
|
B[["Scheduled pipeline<br>`pages` and<br>`pages:deploy` job"]]
|
||||||
C([docs.gitlab.com])
|
C([docs.gitlab.com])
|
||||||
|
|
|
@ -8,7 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
GitLab uses [Akismet](https://akismet.com/) to prevent the creation of
|
GitLab uses [Akismet](https://akismet.com/) to prevent the creation of
|
||||||
spam issues on public projects. Issues created through the web UI or the API can be submitted to
|
spam issues on public projects. Issues created through the web UI or the API can be submitted to
|
||||||
Akismet for review.
|
Akismet for review, and instance administrators can
|
||||||
|
[mark snippets as spam](../user/snippets.md#mark-snippet-as-spam).
|
||||||
|
|
||||||
Detected spam is rejected, and an entry is added in the **Spam Log** section of the
|
Detected spam is rejected, and an entry is added in the **Spam Log** section of the
|
||||||
Admin page.
|
Admin page.
|
||||||
|
|
|
@ -954,6 +954,11 @@ An on-demand scan can be run in active or passive mode:
|
||||||
minimize the risk of accidental damage, running an active scan requires a [validated site
|
minimize the risk of accidental damage, running an active scan requires a [validated site
|
||||||
profile](#site-profile-validation).
|
profile](#site-profile-validation).
|
||||||
|
|
||||||
|
### View on-demand DAST scans
|
||||||
|
|
||||||
|
To view running and completed on-demand DAST scans for a project, go to
|
||||||
|
**Security & Compliance > On-demand Scans** in the left sidebar.
|
||||||
|
|
||||||
### Run an on-demand DAST scan
|
### Run an on-demand DAST scan
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
@ -987,6 +992,7 @@ To run an on-demand scan either at a scheduled date or frequency, read
|
||||||
|
|
||||||
1. From your project's home page, go to **Security & Compliance > On-demand Scans** in the left
|
1. From your project's home page, go to **Security & Compliance > On-demand Scans** in the left
|
||||||
sidebar.
|
sidebar.
|
||||||
|
1. Select **New DAST scan**.
|
||||||
1. Complete the **Scan name** and **Description** fields.
|
1. Complete the **Scan name** and **Description** fields.
|
||||||
1. In GitLab 13.10 and later, select the desired branch from the **Branch** dropdown.
|
1. In GitLab 13.10 and later, select the desired branch from the **Branch** dropdown.
|
||||||
1. In **Scanner profile**, select a scanner profile from the dropdown.
|
1. In **Scanner profile**, select a scanner profile from the dropdown.
|
||||||
|
@ -1023,6 +1029,7 @@ To schedule a scan:
|
||||||
|
|
||||||
1. On the top bar, select **Menu > Projects** and find your project.
|
1. On the top bar, select **Menu > Projects** and find your project.
|
||||||
1. On the left sidebar, select **Security & Compliance > On-demand Scans**.
|
1. On the left sidebar, select **Security & Compliance > On-demand Scans**.
|
||||||
|
1. Select **New DAST scan**.
|
||||||
1. Complete the **Scan name** and **Description** text boxes.
|
1. Complete the **Scan name** and **Description** text boxes.
|
||||||
1. In GitLab 13.10 and later, from the **Branch** dropdown list, select the desired branch.
|
1. In GitLab 13.10 and later, from the **Branch** dropdown list, select the desired branch.
|
||||||
1. In the **Scanner profile** section, from the dropdown list, select a scanner profile.
|
1. In the **Scanner profile** section, from the dropdown list, select a scanner profile.
|
||||||
|
|
|
@ -19,6 +19,7 @@ You can report a user through their:
|
||||||
- [Profile](#reporting-abuse-through-a-users-profile)
|
- [Profile](#reporting-abuse-through-a-users-profile)
|
||||||
- [Comments](#reporting-abuse-through-a-users-comment)
|
- [Comments](#reporting-abuse-through-a-users-comment)
|
||||||
- [Issues and Merge requests](#reporting-abuse-through-a-users-issue-or-merge-request)
|
- [Issues and Merge requests](#reporting-abuse-through-a-users-issue-or-merge-request)
|
||||||
|
- [Snippets](snippets.md#mark-snippet-as-spam)
|
||||||
|
|
||||||
## Reporting abuse through a user's profile
|
## Reporting abuse through a user's profile
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,24 @@ snippet was created using the GitLab web interface the original line ending is W
|
||||||
With snippets, you engage in a conversation about that piece of code,
|
With snippets, you engage in a conversation about that piece of code,
|
||||||
which can encourage user collaboration.
|
which can encourage user collaboration.
|
||||||
|
|
||||||
|
## Mark snippet as spam **(FREE SELF)**
|
||||||
|
|
||||||
|
Administrators on self-managed GitLab instances can mark snippets as spam.
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- You must be the administrator for your instance.
|
||||||
|
- [Akismet](../integration/akismet.md) spam protection must be enabled on the instance.
|
||||||
|
|
||||||
|
To do this task:
|
||||||
|
|
||||||
|
1. On the top bar, select **Menu > Projects** and find your project.
|
||||||
|
1. On the left sidebar, select **Snippets**.
|
||||||
|
1. Select the snippet you want to report as spam.
|
||||||
|
1. Select **Submit as spam**.
|
||||||
|
|
||||||
|
GitLab forwards the spam to Akismet.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Snippet limitations
|
### Snippet limitations
|
||||||
|
|
42
lib/bulk_imports/common/pipelines/wiki_pipeline.rb
Normal file
42
lib/bulk_imports/common/pipelines/wiki_pipeline.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module BulkImports
|
||||||
|
module Common
|
||||||
|
module Pipelines
|
||||||
|
class WikiPipeline
|
||||||
|
include Pipeline
|
||||||
|
|
||||||
|
def extract(*)
|
||||||
|
BulkImports::Pipeline::ExtractedData.new(data: { url: url_from_parent_path(context.entity.source_full_path) })
|
||||||
|
end
|
||||||
|
|
||||||
|
def transform(_, data)
|
||||||
|
data&.slice(:url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load(context, data)
|
||||||
|
return unless context.portable.wiki
|
||||||
|
|
||||||
|
url = data[:url].sub("://", "://oauth2:#{context.configuration.access_token}@")
|
||||||
|
|
||||||
|
Gitlab::UrlBlocker.validate!(url, allow_local_network: allow_local_requests?, allow_localhost: allow_local_requests?)
|
||||||
|
|
||||||
|
context.portable.wiki.ensure_repository
|
||||||
|
context.portable.wiki.repository.fetch_as_mirror(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def url_from_parent_path(parent_path)
|
||||||
|
wiki_path = parent_path + ".wiki.git"
|
||||||
|
root = context.configuration.url
|
||||||
|
Gitlab::Utils.append_path(root, wiki_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def allow_local_requests?
|
||||||
|
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -35,6 +35,10 @@ module BulkImports
|
||||||
pipeline: BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline,
|
pipeline: BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline,
|
||||||
stage: 4
|
stage: 4
|
||||||
},
|
},
|
||||||
|
wiki: {
|
||||||
|
pipeline: BulkImports::Common::Pipelines::WikiPipeline,
|
||||||
|
stage: 5
|
||||||
|
},
|
||||||
uploads: {
|
uploads: {
|
||||||
pipeline: BulkImports::Common::Pipelines::UploadsPipeline,
|
pipeline: BulkImports::Common::Pipelines::UploadsPipeline,
|
||||||
stage: 5
|
stage: 5
|
||||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
||||||
UnknownTrackError = Class.new(StandardError)
|
UnknownTrackError = Class.new(StandardError)
|
||||||
|
|
||||||
def self.for(track)
|
def self.for(track)
|
||||||
valid_tracks = [:invite_team, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
|
valid_tracks = [Namespaces::InviteTeamEmailService::TRACK, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
|
||||||
raise UnknownTrackError unless valid_tracks.include?(track)
|
raise UnknownTrackError unless valid_tracks.include?(track)
|
||||||
|
|
||||||
"Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize
|
"Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize
|
||||||
|
|
|
@ -177,8 +177,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_series!
|
def validate_series!
|
||||||
return unless series?
|
|
||||||
|
|
||||||
raise ArgumentError, "Only #{total_series} series available for this track." unless @series.between?(0, total_series - 1)
|
raise ArgumentError, "Only #{total_series} series available for this track." unless @series.between?(0, total_series - 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,6 +40,12 @@ module Gitlab
|
||||||
def series?
|
def series?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_series!
|
||||||
|
raise ArgumentError, "Only one email is sent for this track. Value of `series` should be 0." unless @series == 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -829,7 +829,13 @@ module Gitlab
|
||||||
|
|
||||||
Users::InProductMarketingEmail.tracks.keys.each_with_object({}) do |track, result|
|
Users::InProductMarketingEmail.tracks.keys.each_with_object({}) do |track, result|
|
||||||
# rubocop: enable UsageData/LargeTable:
|
# rubocop: enable UsageData/LargeTable:
|
||||||
series_amount = Namespaces::InProductMarketingEmailsService::TRACKS[track.to_sym][:interval_days].count
|
series_amount =
|
||||||
|
if track.to_sym == Namespaces::InviteTeamEmailService::TRACK
|
||||||
|
0
|
||||||
|
else
|
||||||
|
Namespaces::InProductMarketingEmailsService::TRACKS[track.to_sym][:interval_days].count
|
||||||
|
end
|
||||||
|
|
||||||
0.upto(series_amount - 1).map do |series|
|
0.upto(series_amount - 1).map do |series|
|
||||||
# When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
|
# When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
|
||||||
sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
|
sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
|
||||||
|
|
25
spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
Normal file
25
spec/lib/bulk_imports/common/pipelines/wiki_pipeline_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe BulkImports::Common::Pipelines::WikiPipeline do
|
||||||
|
describe '#run' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
|
||||||
|
let_it_be(:parent) { create(:project) }
|
||||||
|
|
||||||
|
let_it_be(:entity) do
|
||||||
|
create(
|
||||||
|
:bulk_import_entity,
|
||||||
|
:project_entity,
|
||||||
|
bulk_import: bulk_import,
|
||||||
|
source_full_path: 'source/full/path',
|
||||||
|
destination_name: 'My Destination Wiki',
|
||||||
|
destination_namespace: parent.full_path,
|
||||||
|
project: parent
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'wiki pipeline imports a wiki for an entity'
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,7 @@ RSpec.describe BulkImports::Projects::Stage do
|
||||||
[4, BulkImports::Common::Pipelines::BoardsPipeline],
|
[4, BulkImports::Common::Pipelines::BoardsPipeline],
|
||||||
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
|
[4, BulkImports::Projects::Pipelines::MergeRequestsPipeline],
|
||||||
[4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
|
[4, BulkImports::Projects::Pipelines::ExternalPullRequestsPipeline],
|
||||||
|
[5, BulkImports::Common::Pipelines::WikiPipeline],
|
||||||
[5, BulkImports::Common::Pipelines::UploadsPipeline],
|
[5, BulkImports::Common::Pipelines::UploadsPipeline],
|
||||||
[6, BulkImports::Common::Pipelines::EntityFinisher]
|
[6, BulkImports::Common::Pipelines::EntityFinisher]
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,25 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::InviteTeam do
|
||||||
let_it_be(:group) { build(:group) }
|
let_it_be(:group) { build(:group) }
|
||||||
let_it_be(:user) { build(:user) }
|
let_it_be(:user) { build(:user) }
|
||||||
|
|
||||||
subject(:message) { described_class.new(group: group, user: user, series: 0) }
|
let(:series) { 0 }
|
||||||
|
|
||||||
|
subject(:message) { described_class.new(group: group, user: user, series: series) }
|
||||||
|
|
||||||
|
describe 'initialize' do
|
||||||
|
context 'when series is valid' do
|
||||||
|
it 'does not raise error' do
|
||||||
|
expect { subject }.not_to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when series is invalid' do
|
||||||
|
let(:series) { 1 }
|
||||||
|
|
||||||
|
it 'raises error' do
|
||||||
|
expect { subject }.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'contains the correct message', :aggregate_failures do
|
it 'contains the correct message', :aggregate_failures do
|
||||||
expect(message.subject_line).to eq 'Invite your teammates to GitLab'
|
expect(message.subject_line).to eq 'Invite your teammates to GitLab'
|
||||||
|
|
|
@ -21,7 +21,8 @@ RSpec.describe Users::InProductMarketingEmail, type: :model do
|
||||||
|
|
||||||
describe '.tracks' do
|
describe '.tracks' do
|
||||||
it 'has an entry for every track' do
|
it 'has an entry for every track' do
|
||||||
expect(Namespaces::InProductMarketingEmailsService::TRACKS.keys).to match_array(described_class.tracks.keys.map(&:to_sym))
|
tracks = [Namespaces::InviteTeamEmailService::TRACK, Namespaces::InProductMarketingEmailsService::TRACKS.keys].flatten
|
||||||
|
expect(tracks).to match_array(described_class.tracks.keys.map(&:to_sym))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,7 @@ RSpec.describe Groups::EmailCampaignsController do
|
||||||
|
|
||||||
describe 'track parameter' do
|
describe 'track parameter' do
|
||||||
context 'when valid' do
|
context 'when valid' do
|
||||||
where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience))
|
where(track: [Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience), Namespaces::InviteTeamEmailService::TRACK].flatten)
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
it_behaves_like 'track and redirect'
|
it_behaves_like 'track and redirect'
|
||||||
|
@ -117,6 +117,10 @@ RSpec.describe Groups::EmailCampaignsController do
|
||||||
with_them do
|
with_them do
|
||||||
it_behaves_like 'track and redirect'
|
it_behaves_like 'track and redirect'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'track and redirect' do
|
||||||
|
let(:track) { Namespaces::InviteTeamEmailService::TRACK.to_s }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when invalid' do
|
context 'when invalid' do
|
||||||
|
@ -124,6 +128,10 @@ RSpec.describe Groups::EmailCampaignsController do
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
it_behaves_like 'no track and 404'
|
it_behaves_like 'no track and 404'
|
||||||
|
|
||||||
|
it_behaves_like 'no track and 404' do
|
||||||
|
let(:track) { Namespaces::InviteTeamEmailService::TRACK.to_s }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -242,4 +242,41 @@ RSpec.describe Groups::CreateService, '#execute' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'invite team email' do
|
||||||
|
let(:service) { described_class.new(user, group_params) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Namespaces::InviteTeamEmailWorker).to receive(:perform_in)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is sent' do
|
||||||
|
group = service.execute
|
||||||
|
delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES
|
||||||
|
expect(Namespaces::InviteTeamEmailWorker).to have_received(:perform_in).with(delay, group.id, user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group has not been persisted' do
|
||||||
|
let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
|
||||||
|
|
||||||
|
it 'not sent' do
|
||||||
|
expect(Namespaces::InviteTeamEmailWorker).not_to receive(:perform_in)
|
||||||
|
service.execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group is not root' do
|
||||||
|
let(:parent_group) { create :group }
|
||||||
|
let(:service) { described_class.new(user, group_params.merge(parent_id: parent_group.id)) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
parent_group.add_owner(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'not sent' do
|
||||||
|
expect(Namespaces::InviteTeamEmailWorker).not_to receive(:perform_in)
|
||||||
|
service.execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Namespaces::InProductMarketingEmailRecords do
|
||||||
|
let_it_be(:user) { create :user }
|
||||||
|
|
||||||
|
subject(:records) { described_class.new }
|
||||||
|
|
||||||
|
it 'initializes records' do
|
||||||
|
expect(subject.records).to match_array []
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#save!' do
|
||||||
|
before do
|
||||||
|
allow(Users::InProductMarketingEmail).to receive(:bulk_insert!)
|
||||||
|
|
||||||
|
records.add(user, :invite_team, 0)
|
||||||
|
records.add(user, :create, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'bulk inserts added records' do
|
||||||
|
expect(Users::InProductMarketingEmail).to receive(:bulk_insert!).with(records.records)
|
||||||
|
records.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resets its records' do
|
||||||
|
records.save!
|
||||||
|
expect(records.records).to match_array []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#add' do
|
||||||
|
it 'adds a Users::InProductMarketingEmail record to its records' do
|
||||||
|
freeze_time do
|
||||||
|
records.add(user, :invite_team, 0)
|
||||||
|
records.add(user, :create, 1)
|
||||||
|
|
||||||
|
first, second = records.records
|
||||||
|
|
||||||
|
expect(first).to be_a Users::InProductMarketingEmail
|
||||||
|
expect(first.track.to_sym).to eq :invite_team
|
||||||
|
expect(first.series).to eq 0
|
||||||
|
expect(first.created_at).to eq Time.zone.now
|
||||||
|
expect(first.updated_at).to eq Time.zone.now
|
||||||
|
|
||||||
|
expect(second).to be_a Users::InProductMarketingEmail
|
||||||
|
expect(second.track.to_sym).to eq :create
|
||||||
|
expect(second.series).to eq 1
|
||||||
|
expect(second.created_at).to eq Time.zone.now
|
||||||
|
expect(second.updated_at).to eq Time.zone.now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
128
spec/services/namespaces/invite_team_email_service_spec.rb
Normal file
128
spec/services/namespaces/invite_team_email_service_spec.rb
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Namespaces::InviteTeamEmailService do
|
||||||
|
let_it_be(:user) { create(:user, email_opted_in: true) }
|
||||||
|
|
||||||
|
let(:track) { described_class::TRACK }
|
||||||
|
let(:series) { 0 }
|
||||||
|
|
||||||
|
let(:setup_for_company) { true }
|
||||||
|
let(:parent_group) { nil }
|
||||||
|
let(:group) { create(:group, parent: parent_group) }
|
||||||
|
|
||||||
|
subject(:action) { described_class.send_email(user, group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.add_owner(user)
|
||||||
|
allow(group).to receive(:setup_for_company).and_return(setup_for_company)
|
||||||
|
allow(Notify).to receive(:in_product_marketing_email).and_return(double(deliver_later: nil))
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec::Matchers.define :send_invite_team_email do |*args|
|
||||||
|
match do
|
||||||
|
expect(Notify).to have_received(:in_product_marketing_email).with(*args).once
|
||||||
|
end
|
||||||
|
|
||||||
|
match_when_negated do
|
||||||
|
expect(Notify).not_to have_received(:in_product_marketing_email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'unexperimented' do
|
||||||
|
it { is_expected.not_to send_invite_team_email }
|
||||||
|
|
||||||
|
it 'does not record sent email' do
|
||||||
|
expect { subject }.not_to change { Users::InProductMarketingEmail.count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'candidate' do
|
||||||
|
it { is_expected.to send_invite_team_email(user.id, group.id, track, 0) }
|
||||||
|
|
||||||
|
it 'records sent email' do
|
||||||
|
expect { subject }.to change { Users::InProductMarketingEmail.count }.by(1)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Users::InProductMarketingEmail.where(
|
||||||
|
user: user,
|
||||||
|
track: track,
|
||||||
|
series: 0
|
||||||
|
)
|
||||||
|
).to exist
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'tracks assignment and records the subject', :invite_team_email, :group do
|
||||||
|
subject { group }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group is in control path' do
|
||||||
|
before do
|
||||||
|
stub_experiments(invite_team_email: :control)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.not_to send_invite_team_email }
|
||||||
|
|
||||||
|
it 'does not record sent email' do
|
||||||
|
expect { subject }.not_to change { Users::InProductMarketingEmail.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'tracks assignment and records the subject', :invite_team_email, :group do
|
||||||
|
subject { group }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group is in candidate path' do
|
||||||
|
before do
|
||||||
|
stub_experiments(invite_team_email: :candidate)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'candidate'
|
||||||
|
|
||||||
|
context 'when the user has not opted into marketing emails' do
|
||||||
|
let(:user) { create(:user, email_opted_in: false ) }
|
||||||
|
|
||||||
|
it_behaves_like 'unexperimented'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group is not top level' do
|
||||||
|
it_behaves_like 'unexperimented' do
|
||||||
|
let(:parent_group) do
|
||||||
|
create(:group).tap { |g| g.add_owner(user) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group is not set up for a company' do
|
||||||
|
it_behaves_like 'unexperimented' do
|
||||||
|
let(:setup_for_company) { nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when other users have already been added to the group' do
|
||||||
|
before do
|
||||||
|
group.add_developer(create(:user))
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unexperimented'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when other users have already been invited to the group' do
|
||||||
|
before do
|
||||||
|
group.add_developer('not_a_user_yet@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unexperimented'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the user already got sent the email' do
|
||||||
|
before do
|
||||||
|
create(:in_product_marketing_email, user: user, track: track, series: 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'unexperimented'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do
|
||||||
|
describe '#run' do
|
||||||
|
let_it_be(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
|
||||||
|
|
||||||
|
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
|
||||||
|
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
|
||||||
|
|
||||||
|
let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: {}) }
|
||||||
|
|
||||||
|
context 'successfully imports wiki for an entity' do
|
||||||
|
subject { described_class.new(context) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
|
||||||
|
allow(extractor).to receive(:extract).and_return(extracted_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports new wiki into destination project' do
|
||||||
|
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
|
||||||
|
url = "https://oauth2:token@gitlab.example/#{entity.source_full_path}.wiki.git"
|
||||||
|
expect(repository_service).to receive(:fetch_remote).with(url, any_args).and_return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
subject.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
spec/workers/namespaces/invite_team_email_worker_spec.rb
Normal file
27
spec/workers/namespaces/invite_team_email_worker_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Namespaces::InviteTeamEmailWorker do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
|
it 'sends the email' do
|
||||||
|
expect(Namespaces::InviteTeamEmailService).to receive(:send_email).with(user, group).once
|
||||||
|
subject.perform(group.id, user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user id is non-existent' do
|
||||||
|
it 'does not send the email' do
|
||||||
|
expect(Namespaces::InviteTeamEmailService).not_to receive(:send_email)
|
||||||
|
subject.perform(group.id, non_existing_record_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when group id is non-existent' do
|
||||||
|
it 'does not send the email' do
|
||||||
|
expect(Namespaces::InviteTeamEmailService).not_to receive(:send_email)
|
||||||
|
subject.perform(non_existing_record_id, user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue