Commit cb4bd170 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents e6bf45f1 463d889e
...@@ -927,13 +927,6 @@ Style/RedundantRegexpEscape: ...@@ -927,13 +927,6 @@ Style/RedundantRegexpEscape:
Style/RedundantSelf: Style/RedundantSelf:
Enabled: false Enabled: false
# Offense count: 2
# Cop supports --auto-correct.
Style/RedundantSelfAssignment:
Exclude:
- 'app/models/concerns/issuable.rb'
- 'spec/db/schema_spec.rb'
# Offense count: 213 # Offense count: 213
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes. # Configuration parameters: EnforcedStyle, AllowInnerSlashes.
......
...@@ -119,8 +119,8 @@ export default { ...@@ -119,8 +119,8 @@ export default {
<gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button> <gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
<gl-button <gl-button
:disabled="!canSubmit" :disabled="!canSubmit"
category="primary" category="secondary"
variant="warning" variant="danger"
@click="onSecondaryAction" @click="onSecondaryAction"
> >
{{ secondaryAction }} {{ secondaryAction }}
......
...@@ -324,7 +324,7 @@ module Issuable ...@@ -324,7 +324,7 @@ module Issuable
# This prevents errors when ignored columns are present in the database. # This prevents errors when ignored columns are present in the database.
issuable_columns = with_cte ? issue_grouping_columns(use_cte: with_cte) : "#{table_name}.*" issuable_columns = with_cte ? issue_grouping_columns(use_cte: with_cte) : "#{table_name}.*"
extra_select_columns = extra_select_columns.unshift("(#{highest_priority}) AS highest_priority") extra_select_columns.unshift("(#{highest_priority}) AS highest_priority")
select(issuable_columns) select(issuable_columns)
.select(extra_select_columns) .select(extra_select_columns)
......
...@@ -7,7 +7,7 @@ class GroupMember < Member ...@@ -7,7 +7,7 @@ class GroupMember < Member
SOURCE_TYPE = 'Namespace' SOURCE_TYPE = 'Namespace'
belongs_to :group, foreign_key: 'source_id' belongs_to :group, foreign_key: 'source_id'
alias_attribute :namespace_id, :source_id
delegate :update_two_factor_requirement, to: :user delegate :update_two_factor_requirement, to: :user
# Make sure group member points only to group as it source # Make sure group member points only to group as it source
......
...@@ -5,6 +5,8 @@ class ProjectMember < Member ...@@ -5,6 +5,8 @@ class ProjectMember < Member
belongs_to :project, foreign_key: 'source_id' belongs_to :project, foreign_key: 'source_id'
delegate :namespace_id, to: :project
# Make sure project member points only to project as it source # Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\AProject\z/ } validates :source_type, format: { with: /\AProject\z/ }
......
...@@ -10,13 +10,14 @@ module Members ...@@ -10,13 +10,14 @@ module Members
@errors = {} @errors = {}
@emails = params[:email]&.split(',')&.uniq&.flatten @emails = params[:email]&.split(',')&.uniq&.flatten
@source = params[:source]
end end
def execute(source) def execute
@source = source
validate_emails! validate_emails!
emails.each(&method(:process_email)) emails.each(&method(:process_email))
enqueue_onboarding_progress_action
result result
rescue BlankEmailsError, TooManyEmailsError => e rescue BlankEmailsError, TooManyEmailsError => e
error(e.message) error(e.message)
...@@ -24,7 +25,7 @@ module Members ...@@ -24,7 +25,7 @@ module Members
private private
attr_reader :source, :errors, :emails attr_reader :source, :errors, :emails, :member_created_namespace_id
def validate_emails! def validate_emails!
raise BlankEmailsError, s_('AddMember|Email cannot be blank') if emails.blank? raise BlankEmailsError, s_('AddMember|Email cannot be blank') if emails.blank?
...@@ -88,6 +89,7 @@ module Members ...@@ -88,6 +89,7 @@ module Members
errors[email] = new_member.errors.full_messages.to_sentence errors[email] = new_member.errors.full_messages.to_sentence
else else
after_execute(member: new_member) after_execute(member: new_member)
@member_created_namespace_id ||= new_member.namespace_id
end end
end end
...@@ -98,6 +100,12 @@ module Members ...@@ -98,6 +100,12 @@ module Members
success success
end end
end end
def enqueue_onboarding_progress_action
return unless member_created_namespace_id
Namespaces::OnboardingUserAddedWorker.perform_async(member_created_namespace_id)
end
end end
end end
......
---
title: Add enqueueing of Onboarding Progress to the Invite Service
merge_request: 57372
author:
type: other
---
title: Deprecate btn-warning on admin area delete user modal
merge_request: 57761
author:
type: changed
---
title: 'Migration: Add cloud column to licenses'
merge_request: 57781
author:
type: added
---
title: Fixes rubocop offenses Style/RedundantSelfAssignment
merge_request: 57920
author: Shubham Kumar (@imskr)
type: fixed
# frozen_string_literal: true
class AddCloudToLicenses < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :licenses, :cloud, :boolean, default: false
end
end
a435a211d7e8b9a972323769299fc6e537fdeaa127f8db6ab53031901a51ec36
\ No newline at end of file
...@@ -14182,7 +14182,8 @@ CREATE TABLE licenses ( ...@@ -14182,7 +14182,8 @@ CREATE TABLE licenses (
id integer NOT NULL, id integer NOT NULL,
data text NOT NULL, data text NOT NULL,
created_at timestamp without time zone, created_at timestamp without time zone,
updated_at timestamp without time zone updated_at timestamp without time zone,
cloud boolean DEFAULT false
); );
CREATE SEQUENCE licenses_id_seq CREATE SEQUENCE licenses_id_seq
...@@ -35,7 +35,7 @@ This section is for links to information elsewhere in the GitLab documentation. ...@@ -35,7 +35,7 @@ This section is for links to information elsewhere in the GitLab documentation.
- Storing data in another location. - Storing data in another location.
- Destructively reseeding the GitLab database. - Destructively reseeding the GitLab database.
- Guidance around updating packaged PostgreSQL, including how to stop it - Guidance around updating packaged PostgreSQL, including how to stop it
happening automatically. from happening automatically.
- [Information about external PostgreSQL](../postgresql/external.md). - [Information about external PostgreSQL](../postgresql/external.md).
......
...@@ -896,18 +896,56 @@ On GitLab.com, we have DangerBot setup to monitor Product Intelligence related f ...@@ -896,18 +896,56 @@ On GitLab.com, we have DangerBot setup to monitor Product Intelligence related f
On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric. On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
### Optional: Test Prometheus based Usage Ping ### Usage Ping local setup
If the data submitted includes metrics [queried from Prometheus](#prometheus-queries) that you would like to inspect and verify, To set up Usage Ping locally, you must:
then you need to ensure that a Prometheus server is running locally, and that furthermore the respective GitLab components
are exporting metrics to it. If you do not need to test data coming from Prometheus, no further action 1. [Set up local repositories]#(set-up-local-repositories)
1. [Test local setup](#test-local-setup)
1. (Optional) [Test Prometheus-based usage ping](#test-prometheus-based-usage-ping)
#### Set up local repositories
1. Clone and start [GitLab](https://gitlab.com/gitlab-org/gitlab-development-kit).
1. Clone and start [Versions Application](https://gitlab.com/gitlab-services/version-gitlab-com).
Make sure to run `docker-compose up` to start a PostgreSQL and Redis instance.
1. Point GitLab to the Versions Application endpoint instead of the default endpoint:
1. Open [submit_usage_ping_service.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/submit_usage_ping_service.rb#L4) in your local and modified `PRODUCTION_URL`.
1. Set it to the local Versions Application URL `http://localhost:3000/usage_data`.
#### Test local setup
1. Using the `gitlab` Rails console, manually trigger a usage ping:
```ruby
SubmitUsagePingService.new.execute
```
1. Use the `versions` Rails console to check the usage ping was successfully received,
parsed, and stored in the Versions database:
```ruby
UsageData.last
```
### Test Prometheus-based usage ping
If the data submitted includes metrics [queried from Prometheus](#prometheus-queries)
you want to inspect and verify, you must:
- Ensure that a Prometheus server is running locally.
- Ensure the respective GitLab components are exporting metrics to the Prometheus server.
If you do not need to test data coming from Prometheus, no further action
is necessary. Usage Ping should degrade gracefully in the absence of a running Prometheus server. is necessary. Usage Ping should degrade gracefully in the absence of a running Prometheus server.
There are three kinds of components that may export data to Prometheus, and which are included in Usage Ping: Three kinds of components may export data to Prometheus, and are included in Usage Ping:
- [`node_exporter`](https://github.com/prometheus/node_exporter) - Exports node metrics from the host machine - [`node_exporter`](https://github.com/prometheus/node_exporter): Exports node metrics
- [`gitlab-exporter`](https://gitlab.com/gitlab-org/gitlab-exporter) - Exports process metrics from various GitLab components from the host machine.
- various GitLab services such as Sidekiq and the Rails server that export their own metrics - [`gitlab-exporter`](https://gitlab.com/gitlab-org/gitlab-exporter): Exports process metrics
from various GitLab components.
- Other various GitLab services, such as Sidekiq and the Rails server, which export their own metrics.
#### Test with an Omnibus container #### Test with an Omnibus container
......
...@@ -4,6 +4,8 @@ class SubscriptionsController < ApplicationController ...@@ -4,6 +4,8 @@ class SubscriptionsController < ApplicationController
layout 'checkout' layout 'checkout'
skip_before_action :authenticate_user!, only: [:new, :buy_minutes] skip_before_action :authenticate_user!, only: [:new, :buy_minutes]
before_action :load_eligible_groups, only: %i[new buy_minutes]
feature_category :purchase feature_category :purchase
content_security_policy do |p| content_security_policy do |p|
...@@ -44,15 +46,10 @@ class SubscriptionsController < ApplicationController ...@@ -44,15 +46,10 @@ class SubscriptionsController < ApplicationController
def create def create
current_user.update(setup_for_company: true) if params[:setup_for_company] current_user.update(setup_for_company: true) if params[:setup_for_company]
group = params[:selected_group] ? find_group : create_group
if params[:selected_group] return not_found if group.nil?
group = current_user.manageable_groups_eligible_for_subscription.find(params[:selected_group])
else
name = Namespace.clean_name(params[:setup_for_company] ? customer_params[:company] : current_user.name)
path = Namespace.clean_path(name)
group = Groups::CreateService.new(current_user, name: name, path: path).execute
return render json: group.errors.to_json unless group.persisted? return render json: group.errors.to_json unless group.persisted?
end
response = Subscriptions::CreateService.new( response = Subscriptions::CreateService.new(
current_user, current_user,
...@@ -85,6 +82,23 @@ class SubscriptionsController < ApplicationController ...@@ -85,6 +82,23 @@ class SubscriptionsController < ApplicationController
params.require(:subscription).permit(:plan_id, :payment_method_id, :quantity) params.require(:subscription).permit(:plan_id, :payment_method_id, :quantity)
end end
def find_group
selected_group = current_user.manageable_groups.top_most.find(params[:selected_group])
result = GitlabSubscriptions::FilterPurchaseEligibleNamespacesService
.new(user: current_user, namespaces: Array(selected_group))
.execute
result.success? ? result.payload.first : nil
end
def create_group
name = Namespace.clean_name(params[:setup_for_company] ? customer_params[:company] : current_user.name)
path = Namespace.clean_path(name)
Groups::CreateService.new(current_user, name: name, path: path).execute
end
def client def client
Gitlab::SubscriptionPortal::Client Gitlab::SubscriptionPortal::Client
end end
...@@ -99,4 +113,16 @@ class SubscriptionsController < ApplicationController ...@@ -99,4 +113,16 @@ class SubscriptionsController < ApplicationController
store_location_for :user, request.fullpath store_location_for :user, request.fullpath
redirect_to new_user_registration_path(redirect_from: from) redirect_to new_user_registration_path(redirect_from: from)
end end
def load_eligible_groups
return unless current_user
candidate_groups = current_user.manageable_groups.top_most.with_counts(archived: false)
result = GitlabSubscriptions::FilterPurchaseEligibleNamespacesService
.new(user: current_user, namespaces: candidate_groups)
.execute
@eligible_groups = result.success? ? result.payload : []
end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module SubscriptionsHelper module SubscriptionsHelper
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
def subscription_data def subscription_data(eligible_groups)
{ {
setup_for_company: (current_user.setup_for_company == true).to_s, setup_for_company: (current_user.setup_for_company == true).to_s,
full_name: current_user.name, full_name: current_user.name,
...@@ -12,7 +12,7 @@ module SubscriptionsHelper ...@@ -12,7 +12,7 @@ module SubscriptionsHelper
plan_id: params[:plan_id], plan_id: params[:plan_id],
namespace_id: params[:namespace_id], namespace_id: params[:namespace_id],
new_user: new_user?.to_s, new_user: new_user?.to_s,
group_data: group_data.to_json group_data: present_groups(eligible_groups).to_json
} }
end end
...@@ -64,8 +64,8 @@ module SubscriptionsHelper ...@@ -64,8 +64,8 @@ module SubscriptionsHelper
end end
end end
def group_data def present_groups(groups)
current_user.manageable_groups_eligible_for_subscription.with_counts(archived: false).map do |namespace| groups.map do |namespace|
{ {
id: namespace.id, id: namespace.id,
name: namespace.name, name: namespace.name,
......
...@@ -47,10 +47,6 @@ module EE ...@@ -47,10 +47,6 @@ module EE
.where(plans: { name: [nil, *::Plan.default_plans] }) .where(plans: { name: [nil, *::Plan.default_plans] })
end end
scope :eligible_for_subscription, -> do
top_most.in_active_trial.or(top_most.in_default_plan)
end
scope :eligible_for_trial, -> do scope :eligible_for_trial, -> do
left_joins(gitlab_subscription: :hosted_plan) left_joins(gitlab_subscription: :hosted_plan)
.where( .where(
......
...@@ -255,10 +255,6 @@ module EE ...@@ -255,10 +255,6 @@ module EE
.any? .any?
end end
def manageable_groups_eligible_for_subscription
manageable_groups.eligible_for_subscription.order(:name)
end
def manageable_groups_eligible_for_trial def manageable_groups_eligible_for_trial
manageable_groups.eligible_for_trial.order(:name) manageable_groups.eligible_for_trial.order(:name)
end end
......
# frozen_string_literal: true
module GitlabSubscriptions
class FilterPurchaseEligibleNamespacesService
include ::Gitlab::Utils::StrongMemoize
def initialize(user:, namespaces:)
@user = user
@namespaces = namespaces
end
def execute
return success([]) if namespaces.empty?
return missing_user_error if user.nil?
if response[:success]
eligible_ids = response[:data].map { |data| data['id'] }.to_set
data = namespaces.filter { |namespace| eligible_ids.include?(namespace.id) }
success(data)
else
error('Failed to fetch namespaces', response.dig(:data, :errors))
end
end
private
attr_reader :user, :namespaces
def success(payload)
ServiceResponse.success(payload: payload)
end
def error(message, payload = nil)
ServiceResponse.error(message: message, payload: payload)
end
def missing_user_error
message = 'User cannot be nil'
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(message))
error(message)
end
def response
strong_memoize(:response) do
Gitlab::SubscriptionPortal::Client.filter_purchase_eligible_namespaces(user, namespaces)
end
end
end
end
- page_title _('Buy CI Minutes') - page_title _('Buy CI Minutes')
#js-buy-minutes{ data: subscription_data } #js-buy-minutes{ data: subscription_data(@eligible_groups) }
- page_title _('Checkout') - page_title _('Checkout')
#js-new-subscription{ data: subscription_data } #js-new-subscription{ data: subscription_data(@eligible_groups) }
...@@ -88,8 +88,7 @@ module Gitlab ...@@ -88,8 +88,7 @@ module Gitlab
response = execute_graphql_query({ query: query, variables: { tags: plan_tags } })[:data] response = execute_graphql_query({ query: query, variables: { tags: plan_tags } })[:data]
if response['errors'].present? if response['errors'].present?
exception = SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot") track_error(query, response)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, query: query, response: response)
return error return error
end end
...@@ -121,6 +120,39 @@ module Gitlab ...@@ -121,6 +120,39 @@ module Gitlab
end end
end end
def filter_purchase_eligible_namespaces(user, namespaces)
query = <<~GQL
query FilterEligibleNamespaces($customerUid: Int!, $namespaces: [GitlabNamespaceInput!]!) {
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, eligibleForPurchase: true) {
id
}
}
GQL
namespace_data = namespaces.map do |namespace|
{
id: namespace.id,
parentId: namespace.parent_id,
plan: namespace.actual_plan_name,
trial: !!namespace.trial?
}
end
response = http_post(
"graphql",
admin_headers,
{ query: query, variables: { customerUid: user.id, namespaces: namespace_data } }
)[:data]
if response['errors'].blank?
{ success: true, data: response.dig('data', 'namespaceEligibility') }
else
track_error(query, response)
error(response['errors'])
end
end
private private
def execute_graphql_query(params) def execute_graphql_query(params)
...@@ -137,6 +169,14 @@ module Gitlab ...@@ -137,6 +169,14 @@ module Gitlab
EE::SUBSCRIPTIONS_GRAPHQL_URL EE::SUBSCRIPTIONS_GRAPHQL_URL
end end
def track_error(query, response)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot"),
query: query,
response: response
)
end
def error(errors = nil) def error(errors = nil)
{ {
success: false, success: false,
......
...@@ -20,7 +20,7 @@ RSpec.describe SubscriptionsController do ...@@ -20,7 +20,7 @@ RSpec.describe SubscriptionsController do
end end
describe 'GET #new' do describe 'GET #new' do
subject { get :new, params: { plan_id: 'bronze_id' } } subject(:get_new) { get :new, params: { plan_id: 'bronze_id' } }
it_behaves_like 'unauthenticated subscription request', 'checkout' it_behaves_like 'unauthenticated subscription request', 'checkout'
...@@ -31,11 +31,41 @@ RSpec.describe SubscriptionsController do ...@@ -31,11 +31,41 @@ RSpec.describe SubscriptionsController do
it { is_expected.to render_template 'layouts/checkout' } it { is_expected.to render_template 'layouts/checkout' }
it { is_expected.to render_template :new } it { is_expected.to render_template :new }
context 'when there are groups eligible for the subscription' do
let_it_be(:group) { create(:group) }
before do
group.add_owner(user)
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: [group]) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: [group]))
end
end
it 'assigns the eligible groups for the subscription' do
get_new
expect(assigns(:eligible_groups)).to eq [group]
end
end
context 'when there are no eligible groups for the subscription' do
it 'assigns eligible groups as an empty array' do
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: []) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
get_new
expect(assigns(:eligible_groups)).to eq []
end
end
end end
end end
describe 'GET #buy_minutes' do describe 'GET #buy_minutes' do
subject { get :buy_minutes, params: { plan_id: 'bronze_id' } } subject(:buy_minutes) { get :buy_minutes, params: { plan_id: 'bronze_id' } }
it_behaves_like 'unauthenticated subscription request', 'buy_minutes' it_behaves_like 'unauthenticated subscription request', 'buy_minutes'
...@@ -46,6 +76,36 @@ RSpec.describe SubscriptionsController do ...@@ -46,6 +76,36 @@ RSpec.describe SubscriptionsController do
it { is_expected.to render_template 'layouts/checkout' } it { is_expected.to render_template 'layouts/checkout' }
it { is_expected.to render_template :buy_minutes } it { is_expected.to render_template :buy_minutes }
context 'when there are groups eligible for the subscription' do
let_it_be(:group) { create(:group) }
before do
group.add_owner(user)
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: [group]) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: [group]))
end
end
it 'assigns the eligible groups for the subscription' do
buy_minutes
expect(assigns(:eligible_groups)).to eq [group]
end
end
context 'when there are no eligible groups for the subscription' do
it 'assigns eligible groups as an empty array' do
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: []) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
buy_minutes
expect(assigns(:eligible_groups)).to eq []
end
end
end end
context 'with :new_route_ci_minutes_purchase disabled' do context 'with :new_route_ci_minutes_purchase disabled' do
...@@ -218,7 +278,6 @@ RSpec.describe SubscriptionsController do ...@@ -218,7 +278,6 @@ RSpec.describe SubscriptionsController do
end end
context 'when selecting an existing group' do context 'when selecting an existing group' do
let_it_be(:selected_group) { create(:group) }
let(:params) do let(:params) do
{ {
selected_group: selected_group.id, selected_group: selected_group.id,
...@@ -227,8 +286,21 @@ RSpec.describe SubscriptionsController do ...@@ -227,8 +286,21 @@ RSpec.describe SubscriptionsController do
} }
end end
context 'when the selected group is eligible for a new subscription' do
let_it_be(:selected_group) { create(:group) }
before do before do
selected_group.add_owner(user) selected_group.add_owner(user)
allow_next_instance_of(
GitlabSubscriptions::FilterPurchaseEligibleNamespacesService,
user: user,
namespaces: [selected_group]
) do |instance|
allow(instance)
.to receive(:execute)
.and_return(instance_double(ServiceResponse, success?: true, payload: [selected_group]))
end
end end
it 'does not create a group' do it 'does not create a group' do
...@@ -243,6 +315,35 @@ RSpec.describe SubscriptionsController do ...@@ -243,6 +315,35 @@ RSpec.describe SubscriptionsController do
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json) expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
end end
end
context 'when the selected group is ineligible for a new subscription' do
let_it_be(:selected_group) { create(:group) }
before do
selected_group.add_owner(user)
allow_next_instance_of(
GitlabSubscriptions::FilterPurchaseEligibleNamespacesService,
user: user,
namespaces: [selected_group]
) do |instance|
allow(instance)
.to receive(:execute)
.and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
end
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
end
it 'returns a 404 not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when selected group is a sub group' do context 'when selected group is a sub group' do
let(:selected_group) { create(:group, parent: create(:group))} let(:selected_group) { create(:group, parent: create(:group))}
......
...@@ -46,7 +46,7 @@ RSpec.describe SubscriptionsHelper do ...@@ -46,7 +46,7 @@ RSpec.describe SubscriptionsHelper do
group.add_owner(user) group.add_owner(user)
end end
subject { helper.subscription_data } subject { helper.subscription_data([group]) }
it { is_expected.to include(setup_for_company: 'false') } it { is_expected.to include(setup_for_company: 'false') }
it { is_expected.to include(full_name: 'First Last') } it { is_expected.to include(full_name: 'First Last') }
......
...@@ -244,4 +244,100 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do ...@@ -244,4 +244,100 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do
end end
end end
end end
describe '#filter_purchase_eligible_namespaces' do
subject do
client.filter_purchase_eligible_namespaces(user, [user_namespace, group_namespace, subgroup])
end
let_it_be(:user) { create(:user) }
let_it_be(:user_namespace) { user.namespace }
let_it_be(:group_namespace) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group_namespace) }
let(:headers) do
{
"Accept" => "application/json",
"Content-Type" => "application/json",
"X-Admin-Email" => "gl_com_api@gitlab.com",
"X-Admin-Token" => "customer_admin_token"
}
end
let(:variables) do
{
customerUid: user.id,
namespaces: [
{ id: user_namespace.id, parentId: nil, plan: "default", trial: false },
{ id: group_namespace.id, parentId: nil, plan: "default", trial: false },
{ id: subgroup.id, parentId: group_namespace.id, plan: "default", trial: false }
]
}
end
let(:params) do
{
variables: variables,
query: <<~GQL
query FilterEligibleNamespaces($customerUid: Int!, $namespaces: [GitlabNamespaceInput!]!) {
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, eligibleForPurchase: true) {
id
}
}
GQL
}
end
context 'when the response is successful' do
it 'returns the namespace data', :aggregate_failures do
response = {
data: {
'data' => {
'namespaceEligibility' => [{ 'id' => 1 }, { 'id' => 3 }]
}
}
}
expect(client).to receive(:http_post).with('graphql', headers, params).and_return(response)
expect(subject).to eq(success: true, data: [{ 'id' => 1 }, { 'id' => 3 }])
end
end
context 'when the response is unsuccessful' do
it 'returns the error message', :aggregate_failures do
response = {
data: {
"data" => {
"namespaceEligibility" => nil
},
"errors" => [
{
"message" => "You must be logged in to access this resource",
"locations" => [{ "line" => 2, "column" => 3 }],
"path" => ["namespaceEligibility"]
}
]
}
}
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(
a_kind_of(Gitlab::SubscriptionPortal::Client::ResponseError),
query: params[:query], response: response[:data])
expect(client).to receive(:http_post).with('graphql', headers, params).and_return(response)
expect(subject).to eq(
success: false,
errors: [{
"locations" => [{ "column" => 3, "line" => 2 }],
"message" => "You must be logged in to access this resource",
"path" => ["namespaceEligibility"]
}]
)
end
end
end
end end
...@@ -221,92 +221,6 @@ RSpec.describe Namespace do ...@@ -221,92 +221,6 @@ RSpec.describe Namespace do
end end
end end
describe '.eligible_for_subscription' do
let_it_be(:namespace) { create :namespace }
let_it_be(:group) { create :group }
let_it_be(:subgroup) { create(:group, parent: group) }
subject { described_class.eligible_for_subscription.ids }
context 'when there is no subscription' do
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
context 'when there is a subscription' do
context 'with a plan that is eligible for a trial' do
where(plan: ::Plan::PLANS_ELIGIBLE_FOR_TRIAL)
with_them do
context 'and has not yet been trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, namespace: group
create :gitlab_subscription, plan, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
context 'but has already had a trial' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, :expired_trial, namespace: group
create :gitlab_subscription, plan, :expired_trial, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
context 'but is currently being trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, :active_trial, namespace: group
create :gitlab_subscription, plan, :active_trial, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
end
end
context 'in active trial ultimate plan' do
using RSpec::Parameterized::TableSyntax
where(:plan_name) do
[
[::Plan::GOLD],
[::Plan::ULTIMATE]
]
end
with_them do
before do
create :gitlab_subscription, plan_name, :active_trial, namespace: namespace
create :gitlab_subscription, plan_name, :active_trial, namespace: group
create :gitlab_subscription, plan_name, :active_trial, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
end
context 'with a paid plan and not in trial' do
where(plan: ::Plan::PAID_HOSTED_PLANS)
with_them do
context 'and has not yet been trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, namespace: group
end
it { is_expected.to be_empty }
end
end
end
end
end
describe '.eligible_for_trial' do describe '.eligible_for_trial' do
let_it_be(:namespace) { create :namespace } let_it_be(:namespace) { create :namespace }
......
...@@ -1009,88 +1009,6 @@ RSpec.describe User do ...@@ -1009,88 +1009,6 @@ RSpec.describe User do
end end
end end
describe '#manageable_groups_eligible_for_subscription' do
let_it_be(:user) { create(:user) }
let_it_be(:licensed_group) { create(:group_with_plan, plan: :bronze_plan) }
let_it_be(:free_group_z) { create(:group_with_plan, plan: :free_plan, name: 'AZ') }
let_it_be(:free_group_a) { create(:group_with_plan, plan: :free_plan, name: 'AA') }
let_it_be(:sub_group) { create(:group, name: 'SubGroup', parent: free_group_a) }
let_it_be(:trial_group) { create(:group_with_plan, plan: :ultimate_plan, trial_ends_on: Date.current + 1.day, name: 'AB') }
subject { user.manageable_groups_eligible_for_subscription }
context 'user with no groups' do
it { is_expected.to eq [] }
end
context 'owner of a licensed group' do
before do
licensed_group.add_owner(user)
end
it { is_expected.not_to include licensed_group }
end
context 'guest of a free group' do
before do
free_group_a.add_guest(user)
end
it { is_expected.not_to include free_group_a }
end
context 'developer of a free group' do
before do
free_group_a.add_developer(user)
end
it { is_expected.not_to include free_group_a }
end
context 'maintainer of a free group' do
before do
free_group_a.add_maintainer(user)
end
it { is_expected.to include free_group_a }
end
context 'owner of 2 free groups' do
before do
free_group_a.add_owner(user)
free_group_z.add_owner(user)
end
it { is_expected.to eq [free_group_a, free_group_z] }
it { is_expected.not_to include(sub_group) }
end
context 'developer of a trial group' do
before do
trial_group.add_developer(user)
end
it { is_expected.not_to include(trial_group) }
end
context 'owner of a trial group' do
before do
trial_group.add_owner(user)
end
it { is_expected.to include(trial_group) }
end
context 'maintainer of a trial group' do
before do
trial_group.add_maintainer(user)
end
it { is_expected.to include(trial_group) }
end
end
describe '#manageable_groups_eligible_for_trial' do describe '#manageable_groups_eligible_for_trial' do
let_it_be(:user) { create :user } let_it_be(:user) { create :user }
let_it_be(:non_trialed_group_z) { create :group_with_plan, name: 'Zeta', plan: :free_plan } let_it_be(:non_trialed_group_z) { create :group_with_plan, name: 'Zeta', plan: :free_plan }
......
...@@ -9,9 +9,10 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -9,9 +9,10 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let_it_be(:subgroup) { create(:group, parent: root_ancestor) } let_it_be(:subgroup) { create(:group, parent: root_ancestor) }
let_it_be(:subgroup_project) { create(:project, group: subgroup) } let_it_be(:subgroup_project) { create(:project, group: subgroup) }
let(:params) { { email: %w[email@example.org email2@example.org], access_level: Gitlab::Access::GUEST } } let(:base_params) { { access_level: Gitlab::Access::GUEST, source: project } }
let(:params) { { email: %w[email@example.org email2@example.org] } }
subject(:result) { described_class.new(user, params).execute(project) } subject(:result) { described_class.new(user, base_params.merge(params)).execute }
before_all do before_all do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -90,7 +91,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -90,7 +91,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
end end
context 'when there are some invalid members' do context 'when there are some invalid members' do
let(:params) { { email: %w[_bogus_ email2@example.org], access_level: Gitlab::Access::GUEST } } let(:params) { { email: %w[_bogus_ email2@example.org] } }
it 'only creates Audit Events for valid members' do it 'only creates Audit Events for valid members' do
expect { result }.to change { AuditEvent.count }.by(1) expect { result }.to change { AuditEvent.count }.by(1)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::FilterPurchaseEligibleNamespacesService do
describe '#execute' do
let_it_be(:user) { build(:user) }
context 'when no namespaces are supplied' do
it 'returns an empty array', :aggregate_failures do
result = described_class.new(user: user, namespaces: []).execute
expect(result).to be_success
expect(result.payload).to eq []
end
end
context 'when no user is supplied' do
subject(:service) { described_class.new(user: nil, namespaces: [build(:namespace)]) }
it 'logs and returns an error message', :aggregate_failures do
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with an_instance_of(ArgumentError)
result = service.execute
expect(result).to be_error
expect(result.message).to eq 'User cannot be nil'
expect(result.payload).to be_nil
end
end
context 'when the http request fails' do
subject(:service) { described_class.new(user: user, namespaces: [create(:namespace)]) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: false, data: { errors: 'error' }
)
end
it 'returns an error message', :aggregate_failures do
result = service.execute
expect(result).to be_error
expect(result.message).to eq 'Failed to fetch namespaces'
expect(result.payload).to eq 'error'
end
end
context 'when all the namespaces are eligible' do
let(:namespace_1) { create(:namespace) }
let(:namespace_2) { create(:namespace) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: true, data: [{ 'id' => namespace_1.id }, { 'id' => namespace_2.id }]
)
end
it 'does not filter any namespaces', :aggregate_failures do
namespaces = [namespace_1, namespace_2]
result = described_class.new(user: user, namespaces: namespaces).execute
expect(result).to be_success
expect(result.payload).to eq namespaces
end
end
context 'when the user has a namespace ineligible' do
let(:namespace_1) { create(:namespace) }
let(:namespace_2) { create(:namespace) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: true, data: [{ 'id' => namespace_1.id }]
)
end
it 'is filtered from the results', :aggregate_failures do
namespaces = [namespace_1, namespace_2]
result = described_class.new(user: user, namespaces: namespaces).execute
expect(result).to be_success
expect(result.payload).to eq [namespace_1]
end
end
end
end
...@@ -25,11 +25,11 @@ module API ...@@ -25,11 +25,11 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end end
post ":id/invitations" do post ":id/invitations" do
source = find_source(source_type, params[:id]) params[:source] = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, params[:source])
::Members::InviteService.new(current_user, params).execute(source) ::Members::InviteService.new(current_user, params).execute
end end
desc 'Get a list of group or project invitations viewable by the authenticated user' do desc 'Get a list of group or project invitations viewable by the authenticated user' do
......
...@@ -114,7 +114,7 @@ RSpec.describe 'Database schema' do ...@@ -114,7 +114,7 @@ RSpec.describe 'Database schema' do
# postgres and mysql both automatically create an index on the primary # postgres and mysql both automatically create an index on the primary
# key. Also, the rails connection.indexes() method does not return # key. Also, the rails connection.indexes() method does not return
# automatically generated indexes (like the primary key index). # automatically generated indexes (like the primary key index).
first_indexed_column = first_indexed_column.push(primary_key_column) first_indexed_column.push(primary_key_column)
expect(first_indexed_column.uniq).to include(*foreign_keys_columns) expect(first_indexed_column.uniq).to include(*foreign_keys_columns)
end end
......
...@@ -50,11 +50,11 @@ exports[`User Operation confirmation modal renders modal with form included 1`] ...@@ -50,11 +50,11 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
<gl-button-stub <gl-button-stub
buttontextclasses="" buttontextclasses=""
category="primary" category="secondary"
disabled="true" disabled="true"
icon="" icon=""
size="medium" size="medium"
variant="warning" variant="danger"
> >
secondaryAction secondaryAction
......
...@@ -11,15 +11,15 @@ describe('User Operation confirmation modal', () => { ...@@ -11,15 +11,15 @@ describe('User Operation confirmation modal', () => {
let wrapper; let wrapper;
let formSubmitSpy; let formSubmitSpy;
const findButton = (variant) => const findButton = (variant, category) =>
wrapper wrapper
.findAll(GlButton) .findAll(GlButton)
.filter((w) => w.attributes('variant') === variant) .filter((w) => w.attributes('variant') === variant && w.attributes('category') === category)
.at(0); .at(0);
const findForm = () => wrapper.find('form'); const findForm = () => wrapper.find('form');
const findUsernameInput = () => wrapper.find(GlFormInput); const findUsernameInput = () => wrapper.find(GlFormInput);
const findPrimaryButton = () => findButton('danger'); const findPrimaryButton = () => findButton('danger', 'primary');
const findSecondaryButton = () => findButton('warning'); const findSecondaryButton = () => findButton('danger', 'secondary');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token'); const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
const getUsername = () => findUsernameInput().attributes('value'); const getUsername = () => findUsernameInput().attributes('value');
const getMethodParam = () => new FormData(findForm().element).get('_method'); const getMethodParam = () => new FormData(findForm().element).get('_method');
......
...@@ -66,6 +66,12 @@ RSpec.describe GroupMember do ...@@ -66,6 +66,12 @@ RSpec.describe GroupMember do
it_behaves_like 'members notifications', :group it_behaves_like 'members notifications', :group
describe '#namespace_id' do
subject { build(:group_member, source_id: 1).namespace_id }
it { is_expected.to eq 1 }
end
describe '#real_source_type' do describe '#real_source_type' do
subject { create(:group_member).real_source_type } subject { create(:group_member).real_source_type }
......
...@@ -13,6 +13,10 @@ RSpec.describe ProjectMember do ...@@ -13,6 +13,10 @@ RSpec.describe ProjectMember do
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end end
describe 'delegations' do
it { is_expected.to delegate_method(:namespace_id).to(:project) }
end
describe '.access_level_roles' do describe '.access_level_roles' do
it 'returns Gitlab::Access.options' do it 'returns Gitlab::Access.options' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options) expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
......
...@@ -2,29 +2,43 @@ ...@@ -2,29 +2,43 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Members::InviteService, :aggregate_failures do RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
let_it_be(:project_user) { create(:user) } let_it_be(:project_user) { create(:user) }
let_it_be(:namespace) { project.namespace }
let(:params) { {} } let(:params) { {} }
let(:base_params) { { access_level: Gitlab::Access::GUEST } } let(:base_params) { { access_level: Gitlab::Access::GUEST, source: project } }
subject(:result) { described_class.new(user, base_params.merge(params)).execute(project) } subject(:result) { described_class.new(user, base_params.merge(params) ).execute }
context 'when email is previously unused by current members' do context 'when there is a valid member invited' do
let(:params) { { email: 'email@example.org' } } let(:params) { { email: 'email@example.org' } }
it 'successfully creates a member' do it 'successfully creates a member' do
expect { result }.to change(ProjectMember, :count).by(1) expect_to_create_members(count: 1)
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
end end
it_behaves_like 'records an onboarding progress action', :user_added
end
context 'when email is not a valid email' do
let(:params) { { email: '_bogus_' } }
it 'returns an error' do
expect_not_to_create_members
expect(result[:message]['_bogus_']).to eq("Invite email is invalid")
end
it_behaves_like 'does not record an onboarding progress action'
end end
context 'when emails are passed as an array' do context 'when emails are passed as an array' do
let(:params) { { email: %w[email@example.org email2@example.org] } } let(:params) { { email: %w[email@example.org email2@example.org] } }
it 'successfully creates members' do it 'successfully creates members' do
expect { result }.to change(ProjectMember, :count).by(2) expect_to_create_members(count: 2)
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
end end
end end
...@@ -33,33 +47,23 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -33,33 +47,23 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: '' } } let(:params) { { email: '' } }
it 'returns an error' do it 'returns an error' do
expect(result[:status]).to eq(:error) expect_not_to_create_members
expect(result[:message]).to eq('Email cannot be blank') expect(result[:message]).to eq('Email cannot be blank')
end end
end end
context 'when email param is not included' do context 'when email param is not included' do
it 'returns an error' do it 'returns an error' do
expect(result[:status]).to eq(:error) expect_not_to_create_members
expect(result[:message]).to eq('Email cannot be blank') expect(result[:message]).to eq('Email cannot be blank')
end end
end end
context 'when email is not a valid email' do
let(:params) { { email: '_bogus_' } }
it 'returns an error' do
expect { result }.not_to change(ProjectMember, :count)
expect(result[:status]).to eq(:error)
expect(result[:message]['_bogus_']).to eq("Invite email is invalid")
end
end
context 'when duplicate email addresses are passed' do context 'when duplicate email addresses are passed' do
let(:params) { { email: 'email@example.org,email@example.org' } } let(:params) { { email: 'email@example.org,email@example.org' } }
it 'only creates one member per unique address' do it 'only creates one member per unique address' do
expect { result }.to change(ProjectMember, :count).by(1) expect_to_create_members(count: 1)
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
end end
end end
...@@ -71,8 +75,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -71,8 +75,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: emails } } let(:params) { { email: emails } }
it 'limits the number of emails to 100' do it 'limits the number of emails to 100' do
expect { result }.not_to change(ProjectMember, :count) expect_not_to_create_members
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Too many users specified (limit is 100)') expect(result[:message]).to eq('Too many users specified (limit is 100)')
end end
end end
...@@ -81,8 +84,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -81,8 +84,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: 'email@example.org,email2@example.org', limit: 1 } } let(:params) { { email: 'email@example.org,email2@example.org', limit: 1 } }
it 'limits the number of emails to the limit supplied' do it 'limits the number of emails to the limit supplied' do
expect { result }.not_to change(ProjectMember, :count) expect_not_to_create_members
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Too many users specified (limit is 1)') expect(result[:message]).to eq('Too many users specified (limit is 1)')
end end
end end
...@@ -91,7 +93,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -91,7 +93,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: emails, limit: -1 } } let(:params) { { email: emails, limit: -1 } }
it 'does not limit number of emails' do it 'does not limit number of emails' do
expect { result }.to change(ProjectMember, :count).by(101) expect_to_create_members(count: 101)
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
end end
end end
...@@ -101,7 +103,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -101,7 +103,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: project_user.email } } let(:params) { { email: project_user.email } }
it 'adds an existing user to members' do it 'adds an existing user to members' do
expect { result }.to change(ProjectMember, :count).by(1) expect_to_create_members(count: 1)
expect(result[:status]).to eq(:success) expect(result[:status]).to eq(:success)
expect(project.users).to include project_user expect(project.users).to include project_user
end end
...@@ -111,8 +113,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -111,8 +113,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: project_user.email, access_level: -1 } } let(:params) { { email: project_user.email, access_level: -1 } }
it 'returns an error' do it 'returns an error' do
expect { result }.not_to change(ProjectMember, :count) expect_not_to_create_members
expect(result[:status]).to eq(:error)
expect(result[:message][project_user.email]).to eq("Access level is not included in the list") expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
end end
end end
...@@ -122,7 +123,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -122,7 +123,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } } let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } }
it 'adds new email and returns an error for the already invited email' do it 'adds new email and returns an error for the already invited email' do
expect { result }.to change(ProjectMember, :count).by(1) expect_to_create_members(count: 1)
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}") expect(result[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
expect(project.users).to include project_user expect(project.users).to include project_user
...@@ -134,7 +135,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -134,7 +135,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: "#{requested_member.user.email},#{project_user.email}" } } let(:params) { { email: "#{requested_member.user.email},#{project_user.email}" } }
it 'adds new email and returns an error for the already invited email' do it 'adds new email and returns an error for the already invited email' do
expect { result }.to change(ProjectMember, :count).by(1) expect_to_create_members(count: 1)
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message][requested_member.user.email]) expect(result[:message][requested_member.user.email])
.to eq("Member cannot be invited because they already requested to join #{project.name}") .to eq("Member cannot be invited because they already requested to join #{project.name}")
...@@ -147,10 +148,19 @@ RSpec.describe Members::InviteService, :aggregate_failures do ...@@ -147,10 +148,19 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: "#{existing_member.user.email},#{project_user.email}" } } let(:params) { { email: "#{existing_member.user.email},#{project_user.email}" } }
it 'adds new email and returns an error for the already invited email' do it 'adds new email and returns an error for the already invited email' do
expect { result }.to change(ProjectMember, :count).by(1) expect_to_create_members(count: 1)
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message][existing_member.user.email]).to eq("Already a member of #{project.name}") expect(result[:message][existing_member.user.email]).to eq("Already a member of #{project.name}")
expect(project.users).to include project_user expect(project.users).to include project_user
end end
end end
def expect_to_create_members(count:)
expect { result }.to change(ProjectMember, :count).by(count)
end
def expect_not_to_create_members
expect { result }.not_to change(ProjectMember, :count)
expect(result[:status]).to eq(:error)
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment