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:
Style/RedundantSelf:
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
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
......
......@@ -119,8 +119,8 @@ export default {
<gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
<gl-button
:disabled="!canSubmit"
category="primary"
variant="warning"
category="secondary"
variant="danger"
@click="onSecondaryAction"
>
{{ secondaryAction }}
......
......@@ -324,7 +324,7 @@ module Issuable
# This prevents errors when ignored columns are present in the database.
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(extra_select_columns)
......
......@@ -7,7 +7,7 @@ class GroupMember < Member
SOURCE_TYPE = 'Namespace'
belongs_to :group, foreign_key: 'source_id'
alias_attribute :namespace_id, :source_id
delegate :update_two_factor_requirement, to: :user
# Make sure group member points only to group as it source
......
......@@ -5,6 +5,8 @@ class ProjectMember < Member
belongs_to :project, foreign_key: 'source_id'
delegate :namespace_id, to: :project
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
validates :source_type, format: { with: /\AProject\z/ }
......
......@@ -10,13 +10,14 @@ module Members
@errors = {}
@emails = params[:email]&.split(',')&.uniq&.flatten
@source = params[:source]
end
def execute(source)
@source = source
def execute
validate_emails!
emails.each(&method(:process_email))
enqueue_onboarding_progress_action
result
rescue BlankEmailsError, TooManyEmailsError => e
error(e.message)
......@@ -24,7 +25,7 @@ module Members
private
attr_reader :source, :errors, :emails
attr_reader :source, :errors, :emails, :member_created_namespace_id
def validate_emails!
raise BlankEmailsError, s_('AddMember|Email cannot be blank') if emails.blank?
......@@ -88,6 +89,7 @@ module Members
errors[email] = new_member.errors.full_messages.to_sentence
else
after_execute(member: new_member)
@member_created_namespace_id ||= new_member.namespace_id
end
end
......@@ -98,6 +100,12 @@ module Members
success
end
end
def enqueue_onboarding_progress_action
return unless member_created_namespace_id
Namespaces::OnboardingUserAddedWorker.perform_async(member_created_namespace_id)
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 (
id integer NOT NULL,
data text NOT NULL,
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
......@@ -35,7 +35,7 @@ This section is for links to information elsewhere in the GitLab documentation.
- Storing data in another location.
- Destructively reseeding the GitLab database.
- Guidance around updating packaged PostgreSQL, including how to stop it
happening automatically.
from happening automatically.
- [Information about external PostgreSQL](../postgresql/external.md).
......
......@@ -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.
### 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,
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
To set up Usage Ping locally, you must:
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.
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
- [`gitlab-exporter`](https://gitlab.com/gitlab-org/gitlab-exporter) - Exports process metrics from various GitLab components
- various GitLab services such as Sidekiq and the Rails server that export their own metrics
- [`node_exporter`](https://github.com/prometheus/node_exporter): Exports node metrics
from the host machine.
- [`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
......
......@@ -4,6 +4,8 @@ class SubscriptionsController < ApplicationController
layout 'checkout'
skip_before_action :authenticate_user!, only: [:new, :buy_minutes]
before_action :load_eligible_groups, only: %i[new buy_minutes]
feature_category :purchase
content_security_policy do |p|
......@@ -44,15 +46,10 @@ class SubscriptionsController < ApplicationController
def create
current_user.update(setup_for_company: true) if params[:setup_for_company]
group = params[:selected_group] ? find_group : create_group
if params[:selected_group]
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?
end
return not_found if group.nil?
return render json: group.errors.to_json unless group.persisted?
response = Subscriptions::CreateService.new(
current_user,
......@@ -85,6 +82,23 @@ class SubscriptionsController < ApplicationController
params.require(:subscription).permit(:plan_id, :payment_method_id, :quantity)
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
Gitlab::SubscriptionPortal::Client
end
......@@ -99,4 +113,16 @@ class SubscriptionsController < ApplicationController
store_location_for :user, request.fullpath
redirect_to new_user_registration_path(redirect_from: from)
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
......@@ -3,7 +3,7 @@
module SubscriptionsHelper
include ::Gitlab::Utils::StrongMemoize
def subscription_data
def subscription_data(eligible_groups)
{
setup_for_company: (current_user.setup_for_company == true).to_s,
full_name: current_user.name,
......@@ -12,7 +12,7 @@ module SubscriptionsHelper
plan_id: params[:plan_id],
namespace_id: params[:namespace_id],
new_user: new_user?.to_s,
group_data: group_data.to_json
group_data: present_groups(eligible_groups).to_json
}
end
......@@ -64,8 +64,8 @@ module SubscriptionsHelper
end
end
def group_data
current_user.manageable_groups_eligible_for_subscription.with_counts(archived: false).map do |namespace|
def present_groups(groups)
groups.map do |namespace|
{
id: namespace.id,
name: namespace.name,
......
......@@ -47,10 +47,6 @@ module EE
.where(plans: { name: [nil, *::Plan.default_plans] })
end
scope :eligible_for_subscription, -> do
top_most.in_active_trial.or(top_most.in_default_plan)
end
scope :eligible_for_trial, -> do
left_joins(gitlab_subscription: :hosted_plan)
.where(
......
......@@ -255,10 +255,6 @@ module EE
.any?
end
def manageable_groups_eligible_for_subscription
manageable_groups.eligible_for_subscription.order(:name)
end
def manageable_groups_eligible_for_trial
manageable_groups.eligible_for_trial.order(:name)
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')
#js-buy-minutes{ data: subscription_data }
#js-buy-minutes{ data: subscription_data(@eligible_groups) }
- page_title _('Checkout')
#js-new-subscription{ data: subscription_data }
#js-new-subscription{ data: subscription_data(@eligible_groups) }
......@@ -88,8 +88,7 @@ module Gitlab
response = execute_graphql_query({ query: query, variables: { tags: plan_tags } })[:data]
if response['errors'].present?
exception = SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, query: query, response: response)
track_error(query, response)
return error
end
......@@ -121,6 +120,39 @@ module Gitlab
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
def execute_graphql_query(params)
......@@ -137,6 +169,14 @@ module Gitlab
EE::SUBSCRIPTIONS_GRAPHQL_URL
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)
{
success: false,
......
......@@ -20,7 +20,7 @@ RSpec.describe SubscriptionsController do
end
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'
......@@ -31,11 +31,41 @@ RSpec.describe SubscriptionsController do
it { is_expected.to render_template 'layouts/checkout' }
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
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'
......@@ -46,6 +76,36 @@ RSpec.describe SubscriptionsController do
it { is_expected.to render_template 'layouts/checkout' }
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
context 'with :new_route_ci_minutes_purchase disabled' do
......@@ -218,7 +278,6 @@ RSpec.describe SubscriptionsController do
end
context 'when selecting an existing group' do
let_it_be(:selected_group) { create(:group) }
let(:params) do
{
selected_group: selected_group.id,
......@@ -227,21 +286,63 @@ RSpec.describe SubscriptionsController do
}
end
before do
selected_group.add_owner(user)
end
context 'when the selected group is eligible 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: [selected_group]))
end
end
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
end
it 'returns the selected group location in JSON format' do
subject
plan_id = params[:subscription][:plan_id]
quantity = params[:subscription][:quantity]
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
end
end
it 'returns the selected group location in JSON format' do
subject
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
plan_id = params[:subscription][:plan_id]
quantity = params[:subscription][:quantity]
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
end
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
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
......
......@@ -46,7 +46,7 @@ RSpec.describe SubscriptionsHelper do
group.add_owner(user)
end
subject { helper.subscription_data }
subject { helper.subscription_data([group]) }
it { is_expected.to include(setup_for_company: 'false') }
it { is_expected.to include(full_name: 'First Last') }
......
......@@ -244,4 +244,100 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do
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
......@@ -221,92 +221,6 @@ RSpec.describe Namespace do
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
let_it_be(:namespace) { create :namespace }
......
......@@ -1009,88 +1009,6 @@ RSpec.describe User do
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
let_it_be(:user) { create :user }
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
let_it_be(:subgroup) { create(:group, parent: root_ancestor) }
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
project.add_maintainer(user)
......@@ -90,7 +91,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
end
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
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
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
end
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
desc 'Get a list of group or project invitations viewable by the authenticated user' do
......
......@@ -114,7 +114,7 @@ RSpec.describe 'Database schema' do
# postgres and mysql both automatically create an index on the primary
# key. Also, the rails connection.indexes() method does not return
# 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)
end
......
......@@ -50,11 +50,11 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
<gl-button-stub
buttontextclasses=""
category="primary"
category="secondary"
disabled="true"
icon=""
size="medium"
variant="warning"
variant="danger"
>
secondaryAction
......
......@@ -11,15 +11,15 @@ describe('User Operation confirmation modal', () => {
let wrapper;
let formSubmitSpy;
const findButton = (variant) =>
const findButton = (variant, category) =>
wrapper
.findAll(GlButton)
.filter((w) => w.attributes('variant') === variant)
.filter((w) => w.attributes('variant') === variant && w.attributes('category') === category)
.at(0);
const findForm = () => wrapper.find('form');
const findUsernameInput = () => wrapper.find(GlFormInput);
const findPrimaryButton = () => findButton('danger');
const findSecondaryButton = () => findButton('warning');
const findPrimaryButton = () => findButton('danger', 'primary');
const findSecondaryButton = () => findButton('danger', 'secondary');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
const getUsername = () => findUsernameInput().attributes('value');
const getMethodParam = () => new FormData(findForm().element).get('_method');
......
......@@ -66,6 +66,12 @@ RSpec.describe GroupMember do
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
subject { create(:group_member).real_source_type }
......
......@@ -13,6 +13,10 @@ RSpec.describe ProjectMember do
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
describe 'delegations' do
it { is_expected.to delegate_method(:namespace_id).to(:project) }
end
describe '.access_level_roles' do
it 'returns Gitlab::Access.options' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
......
......@@ -2,29 +2,43 @@
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(:user) { project.owner }
let_it_be(:project_user) { create(:user) }
let_it_be(:namespace) { project.namespace }
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' } }
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)
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
context 'when emails are passed as an array' do
let(:params) { { email: %w[email@example.org email2@example.org] } }
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)
end
end
......@@ -33,33 +47,23 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: '' } }
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')
end
end
context 'when email param is not included' 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')
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
let(:params) { { email: 'email@example.org,email@example.org' } }
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)
end
end
......@@ -71,8 +75,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: emails } }
it 'limits the number of emails to 100' do
expect { result }.not_to change(ProjectMember, :count)
expect(result[:status]).to eq(:error)
expect_not_to_create_members
expect(result[:message]).to eq('Too many users specified (limit is 100)')
end
end
......@@ -81,8 +84,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: 'email@example.org,email2@example.org', limit: 1 } }
it 'limits the number of emails to the limit supplied' do
expect { result }.not_to change(ProjectMember, :count)
expect(result[:status]).to eq(:error)
expect_not_to_create_members
expect(result[:message]).to eq('Too many users specified (limit is 1)')
end
end
......@@ -91,7 +93,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: emails, limit: -1 } }
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)
end
end
......@@ -101,7 +103,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: project_user.email } }
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(project.users).to include project_user
end
......@@ -111,8 +113,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: project_user.email, access_level: -1 } }
it 'returns an error' do
expect { result }.not_to change(ProjectMember, :count)
expect(result[:status]).to eq(:error)
expect_not_to_create_members
expect(result[:message][project_user.email]).to eq("Access level is not included in the list")
end
end
......@@ -122,7 +123,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: "#{invited_member.invite_email},#{project_user.email}" } }
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[:message][invited_member.invite_email]).to eq("Member already invited to #{project.name}")
expect(project.users).to include project_user
......@@ -134,7 +135,7 @@ RSpec.describe Members::InviteService, :aggregate_failures do
let(:params) { { email: "#{requested_member.user.email},#{project_user.email}" } }
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[:message][requested_member.user.email])
.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
let(:params) { { email: "#{existing_member.user.email},#{project_user.email}" } }
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[:message][existing_member.user.email]).to eq("Already a member of #{project.name}")
expect(project.users).to include project_user
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
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