Commit 62c6ea0f authored by Alex Buijs's avatar Alex Buijs

Add verification before namespace creation

This adds an experiment for requiring credit card validation
before creating a new group when signing up.

Changelog: added
parent a2aa897b
# frozen_string_literal: true
class RequireVerificationForNamespaceCreationExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def control_behavior
false
end
def candidate_behavior
true
end
def candidate?
run
end
def record_conversion(namespace)
return unless should_track?
Experiment.by_name(name).record_conversion_event_for_subject(subject, namespace_id: namespace.id)
end
private
def subject
context.value[:user]
end
end
...@@ -7,7 +7,7 @@ class Experiment < ApplicationRecord ...@@ -7,7 +7,7 @@ class Experiment < ApplicationRecord
validates :name, presence: true, uniqueness: true, length: { maximum: 255 } validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
def self.add_user(name, group_type, user, context = {}) def self.add_user(name, group_type, user, context = {})
find_or_create_by!(name: name).record_user_and_group(user, group_type, context) by_name(name).record_user_and_group(user, group_type, context)
end end
def self.add_group(name, variant:, group:) def self.add_group(name, variant:, group:)
...@@ -15,11 +15,15 @@ class Experiment < ApplicationRecord ...@@ -15,11 +15,15 @@ class Experiment < ApplicationRecord
end end
def self.add_subject(name, variant:, subject:) def self.add_subject(name, variant:, subject:)
find_or_create_by!(name: name).record_subject_and_variant!(subject, variant) by_name(name).record_subject_and_variant!(subject, variant)
end end
def self.record_conversion_event(name, user, context = {}) def self.record_conversion_event(name, user, context = {})
find_or_create_by!(name: name).record_conversion_event_for_user(user, context) by_name(name).record_conversion_event_for_user(user, context)
end
def self.by_name(name)
find_or_create_by!(name: name)
end end
# Create or update the recorded experiment_user row for the user in this experiment. # Create or update the recorded experiment_user row for the user in this experiment.
...@@ -41,6 +45,16 @@ class Experiment < ApplicationRecord ...@@ -41,6 +45,16 @@ class Experiment < ApplicationRecord
experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context)) experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context))
end end
def record_conversion_event_for_subject(subject, context = {})
raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
attr_name = subject.class.table_name.singularize.to_sym
experiment_subject = experiment_subjects.find_by(attr_name => subject)
return unless experiment_subject
experiment_subject.update!(converted_at: Time.current, context: merged_context(experiment_subject, context))
end
def record_subject_and_variant!(subject, variant) def record_subject_and_variant!(subject, variant)
raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject) raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject)
...@@ -57,7 +71,7 @@ class Experiment < ApplicationRecord ...@@ -57,7 +71,7 @@ class Experiment < ApplicationRecord
private private
def merged_context(experiment_user, new_context) def merged_context(experiment_subject, new_context)
experiment_user.context.deep_merge(new_context.deep_stringify_keys) experiment_subject.context.deep_merge(new_context.deep_stringify_keys)
end end
end end
...@@ -331,6 +331,7 @@ class User < ApplicationRecord ...@@ -331,6 +331,7 @@ class User < ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
delegate :requires_credit_card_verification, :requires_credit_card_verification=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true accepts_nested_attributes_for :user_detail, update_only: true
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
module Users module Users
class UpsertCreditCardValidationService < BaseService class UpsertCreditCardValidationService < BaseService
def initialize(params) def initialize(params, user)
@params = params.to_h.with_indifferent_access @params = params.to_h.with_indifferent_access
@current_user = user
end end
def execute def execute
...@@ -18,6 +19,8 @@ module Users ...@@ -18,6 +19,8 @@ module Users
::Users::CreditCardValidation.upsert(@params) ::Users::CreditCardValidation.upsert(@params)
::Users::UpdateService.new(current_user, user: current_user, requires_credit_card_verification: false).execute!
ServiceResponse.success(message: 'CreditCardValidation was set') ServiceResponse.success(message: 'CreditCardValidation was set')
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}") ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
......
---
name: require_verification_for_namespace_creation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77315
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350251
milestone: '14.8'
type: experiment
group: group::activation
default_enabled: false
# frozen_string_literal: true
class AddRequiresVerificationToUserDetails < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
add_column :user_details, :requires_credit_card_verification, :boolean, null: false, default: false
end
end
1199adba4c13e9234eabadefeb55ed3cfb19e9d5a87c07b90d438e4f48a973f7
\ No newline at end of file
...@@ -20256,6 +20256,7 @@ CREATE TABLE user_details ( ...@@ -20256,6 +20256,7 @@ CREATE TABLE user_details (
pronunciation text, pronunciation text,
registration_objective smallint, registration_objective smallint,
phone text, phone text,
requires_credit_card_verification boolean DEFAULT false NOT NULL,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)), CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 50)), CONSTRAINT check_a73b398c60 CHECK ((char_length(phone) <= 50)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100)), CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100)),
<script>
import { GlButton } from '@gitlab/ui';
import Zuora from 'ee/billings/components/zuora.vue';
import { I18N, IFRAME_MINIMUM_HEIGHT } from '../constants';
import StaticToggle from './static_toggle.vue';
export default {
components: {
GlButton,
StaticToggle,
Zuora,
},
inject: ['completed', 'iframeUrl', 'allowedOrigin'],
data() {
return {
verificationCompleted: this.completed,
};
},
watch: {
verificationCompleted() {
this.toggleProjectCreation();
},
},
mounted() {
this.toggleProjectCreation();
},
methods: {
submit() {
this.$refs.zuora.submit();
},
verified() {
this.verificationCompleted = true;
},
toggleProjectCreation() {
// Workaround until we refactor group and project creation into Vue
// https://gitlab.com/gitlab-org/gitlab/-/issues/339998
const el = document.querySelector('.js-toggle-container');
el.classList.toggle('gl-display-none', !this.verificationCompleted);
},
},
i18n: I18N,
iframeHeight: IFRAME_MINIMUM_HEIGHT,
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full">
<static-toggle
ref="verifyToggle"
:enabled="!verificationCompleted"
:completed="verificationCompleted"
:title="$options.i18n.verifyToggle"
/>
<div
v-if="!verificationCompleted"
class="gl-border-gray-100 gl-border-solid gl-border-1 gl-rounded-base gl-px-2 gl-py-5 gl-text-left"
>
<div class="gl-px-4 gl-text-secondary gl-font-sm">
{{ $options.i18n.explanation }}
</div>
<zuora
ref="zuora"
:initial-height="$options.iframeHeight"
:iframe-url="iframeUrl"
:allowed-origin="allowedOrigin"
@success="verified"
/>
<div class="gl-px-4">
<gl-button
ref="submitButton"
variant="confirm"
type="submit"
class="gl-w-full!"
@click="submit"
>
{{ $options.i18n.submitVerify }}
</gl-button>
</div>
</div>
<static-toggle
ref="createToggle"
:enabled="verificationCompleted"
:title="$options.i18n.createToggle"
/>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
enabled: {
type: Boolean,
required: true,
},
completed: {
type: Boolean,
required: false,
default: false,
},
title: {
type: String,
required: true,
},
},
computed: {
icon() {
return this.enabled ? 'chevron-down' : 'chevron-right';
},
},
i18n: {
label: __('Completed'),
},
};
</script>
<template>
<div class="gl-border-gray-100 gl-border-l-solid gl-border-1 gl-w-full gl-my-3 gl-pl-3">
<div class="gl-display-flex gl-align-items-center">
<span
class="gl-display-flex gl-align-items-center gl-flex-grow-1 gl-font-weight-bold gl-text-blue-500"
:class="{ 'gl-text-gray-400!': !enabled }"
>
<gl-icon :name="icon" :size="24" class="gl-text-gray-500 gl-mr-3" />
{{ title }}
</span>
<gl-icon
v-if="completed"
name="check-circle-filled"
:size="16"
class="gl-text-green-600"
:aria-label="$options.i18n.label"
/>
</div>
</div>
</template>
import { s__ } from '~/locale';
export const IFRAME_MINIMUM_HEIGHT = 312;
export const I18N = {
verifyToggle: s__('IdentityVerification|Verify your identity'),
createToggle: s__('IdentityVerification|Create a project'),
explanation: s__(
'IdentityVerification|Before you create your first project, we need you to verify your identity with a valid payment method. You will not be charged during this step. If we ever need to charge you, we will let you know.',
),
submitVerify: s__('IdentityVerification|Verify your identity'),
};
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { bindHowToImport } from '~/projects/project_new'; import { bindHowToImport } from '~/projects/project_new';
import { displayGroupPath, displayProjectPath } from './path_display'; import { displayGroupPath, displayProjectPath } from './path_display';
import showTooltip from './show_tooltip'; import showTooltip from './show_tooltip';
import CreditCardVerification from './components/credit_card_verification.vue';
const importButtonsSubmit = () => { const importButtonsSubmit = () => {
const buttons = document.querySelectorAll('.js-import-project-buttons a'); const buttons = document.querySelectorAll('.js-import-project-buttons a');
...@@ -33,6 +36,28 @@ const setAutofocus = () => { ...@@ -33,6 +36,28 @@ const setAutofocus = () => {
const mobileTooltipOpts = () => (bp.getBreakpointSize() === 'xs' ? { placement: 'bottom' } : {}); const mobileTooltipOpts = () => (bp.getBreakpointSize() === 'xs' ? { placement: 'bottom' } : {});
const mountVerification = () => {
const el = document.querySelector('.js-credit-card-verification');
if (!el) {
return null;
}
const { completed, iframeUrl, allowedOrigin } = el.dataset;
return new Vue({
el,
provide: {
completed: parseBoolean(completed),
iframeUrl,
allowedOrigin,
},
render(createElement) {
return createElement(CreditCardVerification);
},
});
};
export default () => { export default () => {
displayGroupPath('.js-group-path-source', '.js-group-path-display'); displayGroupPath('.js-group-path-source', '.js-group-path-display');
displayGroupPath('.js-import-group-path-source', '.js-import-group-path-display'); displayGroupPath('.js-import-group-path-source', '.js-import-group-path-display');
...@@ -41,4 +66,5 @@ export default () => { ...@@ -41,4 +66,5 @@ export default () => {
importButtonsSubmit(); importButtonsSubmit();
bindHowToImport(); bindHowToImport();
setAutofocus(); setAutofocus();
mountVerification();
}; };
# frozen_string_literal: true
module Registrations::Verification
extend ActiveSupport::Concern
included do
before_action :require_verification, if: :verification_required?
private
def verification_required?
html_request? &&
request.get? &&
current_user&.requires_credit_card_verification
end
def require_verification
redirect_to new_users_sign_up_groups_project_path
end
def set_requires_verification
::Users::UpdateService.new(current_user, user: current_user, requires_credit_card_verification: true).execute!
end
end
end
...@@ -6,6 +6,8 @@ module EE ...@@ -6,6 +6,8 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do prepended do
include ::Registrations::Verification
around_action :set_current_ip_address around_action :set_current_ip_address
end end
......
...@@ -6,11 +6,17 @@ module Registrations ...@@ -6,11 +6,17 @@ module Registrations
include Registrations::CreateGroup include Registrations::CreateGroup
include OneTrustCSP include OneTrustCSP
skip_before_action :require_verification, only: :new
before_action :set_requires_verification, only: :new, if: -> { helpers.require_verification_experiment.candidate? }
before_action :require_verification, only: [:create, :import], if: -> { current_user.requires_credit_card_verification }
layout 'minimal' layout 'minimal'
feature_category :onboarding feature_category :onboarding
def new def new
helpers.require_verification_experiment.publish_to_database
@group = Group.new(visibility_level: helpers.default_group_visibility) @group = Group.new(visibility_level: helpers.default_group_visibility)
@project = Project.new(namespace: @group) @project = Project.new(namespace: @group)
...@@ -51,6 +57,7 @@ module Registrations ...@@ -51,6 +57,7 @@ module Registrations
success_url = new_trial_path success_url = new_trial_path
end end
helpers.require_verification_experiment.record_conversion(@group)
redirect_to success_url redirect_to success_url
end end
else else
...@@ -66,6 +73,7 @@ module Registrations ...@@ -66,6 +73,7 @@ module Registrations
@group = Groups::CreateService.new(current_user, modified_group_params).execute @group = Groups::CreateService.new(current_user, modified_group_params).execute
if @group.persisted? if @group.persisted?
combined_registration_experiment.track(:create_group, namespace: @group) combined_registration_experiment.track(:create_group, namespace: @group)
helpers.require_verification_experiment.record_conversion(@group)
import_url = URI.join(root_url, params[:import_url], "?namespace_id=#{@group.id}").to_s import_url = URI.join(root_url, params[:import_url], "?namespace_id=#{@group.id}").to_s
redirect_to import_url redirect_to import_url
......
...@@ -38,6 +38,20 @@ module EE ...@@ -38,6 +38,20 @@ module EE
{ next_step_url: url } { next_step_url: url }
end end
def require_verification_experiment
strong_memoize(:require_verification_experiment) do
experiment(:require_verification_for_namespace_creation, user: current_user)
end
end
def credit_card_verification_data
{
completed: current_user.credit_card_validation.present?.to_s,
iframe_url: ::Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL,
allowed_origin: ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
}
end
private private
def redirect_path def redirect_path
......
...@@ -18,7 +18,9 @@ ...@@ -18,7 +18,9 @@
%p.gl-text-center= _('Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more.') %p.gl-text-center= _('Projects help you organize your work. They contain your file repository, issues, merge requests, and so much more.')
.js-toggle-container.gl-w-full - if (verify = require_verification_experiment.candidate?)
.js-credit-card-verification{ data: credit_card_verification_data }
.js-toggle-container.gl-w-full{ class: ('gl-display-none' if verify) }
%ul.nav.nav-tabs.nav-links.gitlab-tabs.js-group-project-tabs{ role: 'tablist' } %ul.nav.nav-tabs.nav-links.gitlab-tabs.js-group-project-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' } %li.nav-item{ role: 'presentation' }
%a#blank-project-tab.nav-link.active{ href: '#blank-project-pane', data: { toggle: 'tab', track_label: 'blank_project', track_action: 'click_tab', track_value: '' }, role: 'tab' } %a#blank-project-tab.nav-link.active{ href: '#blank-project-pane', data: { toggle: 'tab', track_label: 'blank_project', track_action: 'click_tab', track_value: '' }, role: 'tab' }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Registrations::Verification do
controller(ActionController::Base) do
include Registrations::Verification
before_action :set_requires_verification, only: :new
def index
head :ok
end
def create
head :ok
end
def new
head :ok
end
def html_request?
request.format.html?
end
end
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
describe '#require_verification' do
describe 'verification is not required' do
it 'does not redirect' do
get :index
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'verification is required' do
let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
it 'redirects to the new users sign_up groups_project path' do
get :index
expect(response).to redirect_to(new_users_sign_up_groups_project_path)
end
it 'does not redirect on JS requests' do
get :index, format: :js
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not redirect on POST requests' do
post :create
expect(response).to have_gitlab_http_status(:ok)
end
end
end
describe '#set_requires_verification' do
it 'sets the requires_credit_card_verification attribute' do
expect { get :new }.to change { user.reload.requires_credit_card_verification }.to(true)
end
end
end
...@@ -28,6 +28,28 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do ...@@ -28,6 +28,28 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
subject subject
end end
it 'publishes the required verification experiment to the database' do
expect_next_instance_of(RequireVerificationForNamespaceCreationExperiment) do |experiment|
expect(experiment).to receive(:publish_to_database)
end
subject
end
end
end
shared_context 'records a conversion event' do
let_it_be(:experiment) { create(:experiment, name: :require_verification_for_namespace_creation) }
let_it_be(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
before do
stub_experiments(require_verification_for_namespace_creation: true)
end
it 'records a conversion event for the required verification experiment' do
expect { subject }.to change { experiment_subject.reload.converted_at }.from(nil)
.and change { experiment_subject.context }.to include('namespace_id')
end end
end end
...@@ -55,6 +77,8 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do ...@@ -55,6 +77,8 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
it_behaves_like 'hides email confirmation warning' it_behaves_like 'hides email confirmation warning'
it_behaves_like 'records a conversion event'
context 'when group and project can be created' do context 'when group and project can be created' do
it 'creates a group' do it 'creates a group' do
expect { post_create }.to change { Group.count }.by(1) expect { post_create }.to change { Group.count }.by(1)
...@@ -234,6 +258,8 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do ...@@ -234,6 +258,8 @@ RSpec.describe Registrations::GroupsProjectsController, :experiment do
it_behaves_like 'hides email confirmation warning' it_behaves_like 'hides email confirmation warning'
it_behaves_like 'records a conversion event'
context "when a group can't be created" do context "when a group can't be created" do
before do before do
allow_next_instance_of(::Groups::CreateService) do |service| allow_next_instance_of(::Groups::CreateService) do |service|
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import CreditCardVerification from 'ee/registrations/groups_projects/new/components/credit_card_verification.vue';
import { IFRAME_MINIMUM_HEIGHT } from 'ee/registrations/groups_projects/new/constants';
import { setHTMLFixture } from 'helpers/fixtures';
describe('CreditCardVerification', () => {
let wrapper;
let zuoraSubmitSpy;
const IFRAME_URL = 'https://customers.gitlab.com/payment_forms/cc_registration_validation';
const ALLOWED_ORIGIN = 'https://customers.gitlab.com';
const createComponent = (completed = false) => {
wrapper = shallowMount(CreditCardVerification, {
provide: {
completed,
iframeUrl: IFRAME_URL,
allowedOrigin: ALLOWED_ORIGIN,
},
stubs: {
GlButton,
},
});
};
const verifyToggleEnabled = () =>
wrapper.find({ ref: 'verifyToggle' }).attributes('enabled') === 'true';
const createToggleEnabled = () =>
wrapper.find({ ref: 'createToggle' }).attributes('enabled') === 'true';
const findZuora = () => wrapper.find({ ref: 'zuora' });
const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
const toggleContainerHidden = () =>
document.querySelector('.js-toggle-container').classList.contains('gl-display-none');
beforeEach(() => {
setHTMLFixture('<div class="js-toggle-container gl-display-none" />');
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('when the component is mounted', () => {
it('enables the right toggles', () => {
expect(verifyToggleEnabled()).toBe(true);
expect(createToggleEnabled()).toBe(false);
});
it('hides the toggleContainer', () => {
expect(toggleContainerHidden()).toBe(true);
});
it('renders the Zuora component with the right attributes', () => {
expect(findZuora().exists()).toBe(true);
expect(findZuora().attributes()).toMatchObject({
iframeurl: IFRAME_URL,
allowedorigin: ALLOWED_ORIGIN,
initialheight: IFRAME_MINIMUM_HEIGHT.toString(),
});
});
describe('when verification is completed', () => {
beforeEach(() => {
createComponent(true);
});
it('enables the right toggles', () => {
expect(verifyToggleEnabled()).toBe(false);
expect(createToggleEnabled()).toBe(true);
});
it('shows the toggleContainer', () => {
expect(toggleContainerHidden()).toBe(false);
});
it('hides the Zuora component', () => {
expect(findZuora().exists()).toBe(false);
});
});
});
describe('when the submit button is clicked', () => {
beforeEach(() => {
zuoraSubmitSpy = jest.fn();
wrapper.vm.$refs.zuora = { submit: zuoraSubmitSpy };
findSubmitButton().trigger('click');
});
it('calls the submit method of the Zuora component', () => {
expect(zuoraSubmitSpy).toHaveBeenCalled();
});
});
describe('when the Zuora component emits a success event', () => {
beforeEach(() => {
findZuora().vm.$emit('success');
});
it('enables the right toggles', () => {
expect(verifyToggleEnabled()).toBe(false);
expect(createToggleEnabled()).toBe(true);
});
it('shows the toggleContainer', () => {
expect(toggleContainerHidden()).toBe(false);
});
it('hides the Zuora component', () => {
expect(findZuora().exists()).toBe(false);
});
});
});
...@@ -120,4 +120,20 @@ RSpec.describe EE::RegistrationsHelper do ...@@ -120,4 +120,20 @@ RSpec.describe EE::RegistrationsHelper do
end end
end end
end end
describe '#credit_card_verification_data' do
before do
allow(helper).to receive(:current_user).and_return(build(:user))
end
it 'returns the expected data' do
expect(helper.credit_card_verification_data).to eq(
{
completed: 'false',
iframe_url: ::Gitlab::SubscriptionPortal::REGISTRATION_VALIDATION_FORM_URL,
allowed_origin: ::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
}
)
end
end
end end
...@@ -1076,7 +1076,7 @@ module API ...@@ -1076,7 +1076,7 @@ module API
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
service = ::Users::UpsertCreditCardValidationService.new(attrs).execute service = ::Users::UpsertCreditCardValidationService.new(attrs, user).execute
if service.success? if service.success?
present user.credit_card_validation, with: Entities::UserCreditCardValidations present user.credit_card_validation, with: Entities::UserCreditCardValidations
......
...@@ -17832,9 +17832,15 @@ msgstr "" ...@@ -17832,9 +17832,15 @@ msgstr ""
msgid "Identities" msgid "Identities"
msgstr "" msgstr ""
msgid "IdentityVerification|Before you create your first project, we need you to verify your identity with a valid payment method. You will not be charged during this step. If we ever need to charge you, we will let you know."
msgstr ""
msgid "IdentityVerification|Before you create your group, we need you to verify your identity with a valid payment method." msgid "IdentityVerification|Before you create your group, we need you to verify your identity with a valid payment method."
msgstr "" msgstr ""
msgid "IdentityVerification|Create a project"
msgstr ""
msgid "IdentityVerification|Verify your identity" msgid "IdentityVerification|Verify your identity"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
subject(:experiment) { described_class.new(user: user) }
let_it_be(:user) { create(:user) }
describe '#candidate?' do
context 'when experiment subject is candidate' do
before do
stub_experiments(require_verification_for_namespace_creation: :candidate)
end
it 'returns true' do
expect(experiment.candidate?).to eq(true)
end
end
context 'when experiment subject is control' do
before do
stub_experiments(require_verification_for_namespace_creation: :control)
end
it 'returns false' do
expect(experiment.candidate?).to eq(false)
end
end
end
describe '#record_conversion' do
let_it_be(:namespace) { create(:namespace) }
context 'when should_track? is false' do
before do
allow(experiment).to receive(:should_track?).and_return(false)
end
it 'does not record a conversion event' do
expect(experiment.publish_to_database).to be_nil
expect(experiment.record_conversion(namespace)).to be_nil
end
end
context 'when should_track? is true' do
before do
allow(experiment).to receive(:should_track?).and_return(true)
end
it 'records a conversion event' do
experiment_subject = experiment.publish_to_database
expect { experiment.record_conversion(namespace) }.to change { experiment_subject.reload.converted_at }.from(nil)
.and change { experiment_subject.context }.to include('namespace_id' => namespace.id)
end
end
end
end
...@@ -235,6 +235,54 @@ RSpec.describe Experiment do ...@@ -235,6 +235,54 @@ RSpec.describe Experiment do
end end
end end
describe '#record_conversion_event_for_subject' do
let_it_be(:user) { create(:user) }
let_it_be(:experiment) { create(:experiment) }
let_it_be(:context) { { a: 42 } }
subject(:record_conversion) { experiment.record_conversion_event_for_subject(user, context) }
context 'when no existing experiment_subject record exists for the given user' do
it 'does not update or create an experiment_subject record' do
expect { record_conversion }.not_to change { ExperimentSubject.all.to_a }
end
end
context 'when an existing experiment_subject exists for the given user' do
context 'but it has already been converted' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago) }
it 'does not update the converted_at value' do
expect { record_conversion }.not_to change { experiment_subject.converted_at }
end
end
context 'and it has not yet been converted' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
it 'updates the converted_at value' do
expect { record_conversion }.to change { experiment_subject.reload.converted_at }
end
end
context 'with no existing context' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
it 'updates the context' do
expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42)
end
end
context 'with an existing context' do
let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago, context: { b: 1 } ) }
it 'merges the context' do
expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42, 'b' => 1)
end
end
end
end
describe '#record_subject_and_variant!' do describe '#record_subject_and_variant!' do
let_it_be(:subject_to_record) { create(:group) } let_it_be(:subject_to_record) { create(:group) }
let_it_be(:variant) { :control } let_it_be(:variant) { :control }
......
...@@ -83,6 +83,9 @@ RSpec.describe User do ...@@ -83,6 +83,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil } it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil }
it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
end end
describe 'associations' do describe 'associations' do
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Users::UpsertCreditCardValidationService do RSpec.describe Users::UpsertCreditCardValidationService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
let(:user_id) { user.id } let(:user_id) { user.id }
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) } let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
...@@ -21,7 +21,7 @@ RSpec.describe Users::UpsertCreditCardValidationService do ...@@ -21,7 +21,7 @@ RSpec.describe Users::UpsertCreditCardValidationService do
end end
describe '#execute' do describe '#execute' do
subject(:service) { described_class.new(params) } subject(:service) { described_class.new(params, user) }
context 'successfully set credit card validation record for the user' do context 'successfully set credit card validation record for the user' do
context 'when user does not have credit card validation record' do context 'when user does not have credit card validation record' do
...@@ -42,6 +42,10 @@ RSpec.describe Users::UpsertCreditCardValidationService do ...@@ -42,6 +42,10 @@ RSpec.describe Users::UpsertCreditCardValidationService do
expiration_date: Date.new(expiration_year, 1, 31) expiration_date: Date.new(expiration_year, 1, 31)
) )
end end
it 'sets the requires_credit_card_verification attribute on the user to false' do
expect { service.execute }.to change { user.reload.requires_credit_card_verification }.to(false)
end
end end
context 'when user has credit card validation record' do context 'when user has credit card validation record' do
......
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