Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-09 09:10:06 +00:00
parent e9112f3002
commit 1aa9cd3080
39 changed files with 1706 additions and 45 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -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."

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
f46a0dd662a80d38a4e8d3e6c4db05e61563a959b75d30a4c3724ae6afc2647f

View file

@ -0,0 +1 @@
08399bfbf62533c00dfe3ca3434f6be292ec768f053d3b1fde41d2d68de32fe7

File diff suppressed because it is too large Load diff

View file

@ -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**:

View file

@ -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])

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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]
] ]

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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