Commit 53e31ad3 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents a24b1218 397f75b8
...@@ -312,7 +312,7 @@ gem 'gitlab-pg_query', '~> 1.3', require: 'pg_query' ...@@ -312,7 +312,7 @@ gem 'gitlab-pg_query', '~> 1.3', require: 'pg_query'
gem 'premailer-rails', '~> 1.10.3' gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation # LabKit: Tracing and Correlation
gem 'gitlab-labkit', '0.13.1' gem 'gitlab-labkit', '0.13.3'
# I18n # I18n
gem 'ruby_parser', '~> 3.15', require: false gem 'ruby_parser', '~> 3.15', require: false
......
...@@ -434,9 +434,10 @@ GEM ...@@ -434,9 +434,10 @@ GEM
fog-json (~> 1.2.0) fog-json (~> 1.2.0)
mime-types mime-types
ms_rest_azure (~> 0.12.0) ms_rest_azure (~> 0.12.0)
gitlab-labkit (0.13.1) gitlab-labkit (0.13.3)
actionpack (>= 5.0.0, < 6.1.0) actionpack (>= 5.0.0, < 6.1.0)
activesupport (>= 5.0.0, < 6.1.0) activesupport (>= 5.0.0, < 6.1.0)
gitlab-pg_query (~> 1.3)
grpc (~> 1.19) grpc (~> 1.19)
jaeger-client (~> 1.1) jaeger-client (~> 1.1)
opentracing (~> 0.4) opentracing (~> 0.4)
...@@ -1352,7 +1353,7 @@ DEPENDENCIES ...@@ -1352,7 +1353,7 @@ DEPENDENCIES
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5) gitlab-chronic (~> 0.10.5)
gitlab-fog-azure-rm (~> 1.0) gitlab-fog-azure-rm (~> 1.0)
gitlab-labkit (= 0.13.1) gitlab-labkit (= 0.13.3)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
gitlab-mail_room (~> 0.0.7) gitlab-mail_room (~> 0.0.7)
gitlab-markup (~> 1.7.1) gitlab-markup (~> 1.7.1)
......
@import 'mixins_and_variables_and_functions';
@mixin inset-border-1-red-500($important: false) {
box-shadow: inset 0 0 0 $gl-border-size-1 $red-500 if-important($important);
}
.timezone-dropdown {
.dropdown-menu {
@include gl-w-full;
}
.gl-new-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
}
.modal-footer {
@include gl-bg-gray-10;
}
.invalid-dropdown {
.gl-dropdown-toggle {
@include inset-border-1-red-500;
&:hover {
@include inset-border-1-red-500(true);
}
}
}
...@@ -45,7 +45,7 @@ module Registrations ...@@ -45,7 +45,7 @@ module Registrations
end end
def update_params def update_params
params.require(:user).permit(:role, :setup_for_company) params.require(:user).permit(:role, :other_role, :setup_for_company)
end end
def requires_confirmation?(user) def requires_confirmation?(user)
......
# frozen_string_literal: true
module Ci
module PipelineSchedulesHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
name: timezone.name,
offset: timezone.now.utc_offset,
identifier: timezone.tzinfo.identifier
}
end
end
end
end
# frozen_string_literal: true
module TimeZoneHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
identifier: timezone.tzinfo.identifier,
name: timezone.name,
abbr: timezone.tzinfo.strftime('%Z'),
offset: timezone.now.utc_offset,
formatted_offset: timezone.now.formatted_offset
}
end
end
end
...@@ -289,6 +289,7 @@ class User < ApplicationRecord ...@@ -289,6 +289,7 @@ class User < ApplicationRecord
delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
delegate :other_role, :other_role=, to: :user_detail, allow_nil: true
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
......
...@@ -14,8 +14,16 @@ ...@@ -14,8 +14,16 @@
.row .row
.form-group.col-sm-12 .form-group.col-sm-12
= f.label :role, _('Role'), class: 'label-bold' = f.label :role, _('Role'), class: 'label-bold'
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control', autofocus: true = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control js-user-role-dropdown', autofocus: true
.form-text.gl-text-gray-500.gl-mt-3= _('This will help us personalize your onboarding experience.') - if Feature.enabled?(:user_other_role_details)
.row
.form-group.col-sm-12.js-other-role-group{ class: ("hidden") }
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
= f.text_field :other_role, class: 'form-control'
- else
.row
.form-group.col-sm-12
.form-text.gl-text-gray-500.gl-mt-0.gl-line-height-normal.gl-px-1= _('This will help us personalize your onboarding experience.')
= render_if_exists "registrations/welcome/setup_for_company", f: f = render_if_exists "registrations/welcome/setup_for_company", f: f
.row .row
.form-group.col-sm-12.gl-mb-0 .form-group.col-sm-12.gl-mb-0
......
---
title: Add other role column in user details table
merge_request: 45635
author:
type: added
---
title: Avoid creating wiki empty repo when not present in export files
merge_request: 48890
author:
type: changed
...@@ -204,6 +204,7 @@ module Gitlab ...@@ -204,6 +204,7 @@ module Gitlab
config.assets.precompile << "page_bundles/wiki.css" config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/xterm.css" config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/alert_management_settings.css" config.assets.precompile << "page_bundles/alert_management_settings.css"
config.assets.precompile << "page_bundles/oncall_schedules.css"
config.assets.precompile << "lazy_bundles/cropper.css" config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "lazy_bundles/select2.css" config.assets.precompile << "lazy_bundles/select2.css"
config.assets.precompile << "performance_bar.css" config.assets.precompile << "performance_bar.css"
......
---
name: user_other_role_details
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45635
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255170
milestone: '13.7'
type: development
group: group::conversion
default_enabled: false
# frozen_string_literal: true
class AddOtherRoleToUserDetails < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless column_exists?(:user_details, :other_role)
with_lock_retries do
add_column :user_details, :other_role, :text
end
end
add_text_limit :user_details, :other_role, 100
end
def down
with_lock_retries do
remove_column :user_details, :other_role
end
end
end
70fae11d6a73ea8b2ad75c574716f48e9cc78a58ae23db48e74840646fd46672
\ No newline at end of file
...@@ -16961,7 +16961,9 @@ CREATE TABLE user_details ( ...@@ -16961,7 +16961,9 @@ CREATE TABLE user_details (
bio_html text, bio_html text,
cached_markdown_version integer, cached_markdown_version integer,
webauthn_xid text, webauthn_xid text,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)) other_role text,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100))
); );
CREATE SEQUENCE user_details_user_id_seq CREATE SEQUENCE user_details_user_id_seq
......
<script>
import { isEqual, isEmpty } from 'lodash';
import {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import createOncallScheduleMutation from '../graphql/create_oncall_schedule.mutation.graphql';
export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
search: __('Search'),
noResults: __('No matching results'),
cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add schedule'),
fields: {
name: {
title: __('Name'),
validation: {
empty: __("Can't be empty"),
},
},
description: { title: __('Description (optional)') },
timezone: {
title: __('Timezone'),
description: s__(
'OnCallSchedules|Sets the default timezone for the schedule, for all participants',
),
validation: {
empty: __("Can't be empty"),
},
},
},
errorMsg: s__('OnCallSchedules|Failed to add schedule'),
};
export default {
i18n,
inject: ['projectPath', 'timezones'],
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
},
props: {
modalId: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
tzSearchTerm: '',
form: {
name: '',
description: '',
timezone: {},
},
error: null,
};
},
computed: {
actionsProps() {
return {
primary: {
text: i18n.addSchedule,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
{ disabled: this.isFormInvalid },
],
},
cancel: {
text: i18n.cancel,
},
};
},
filteredTimezones() {
const lowerCaseTzSearchTerm = this.tzSearchTerm.toLowerCase();
return this.timezones.filter(tz =>
this.getFormattedTimezone(tz)
.toLowerCase()
.includes(lowerCaseTzSearchTerm),
);
},
noResults() {
return !this.filteredTimezones.length;
},
selectedTimezone() {
return isEmpty(this.form.timezone)
? i18n.selectTimezone
: this.getFormattedTimezone(this.form.timezone);
},
isNameInvalid() {
return !this.form.name.length;
},
isTimezoneInvalid() {
return isEmpty(this.form.timezone);
},
isFormInvalid() {
return this.isNameInvalid || this.isTimezoneInvalid;
},
},
methods: {
createSchedule() {
this.loading = true;
this.$apollo
.mutate({
mutation: createOncallScheduleMutation,
variables: {
oncallScheduleCreateInput: {
projectPath: this.projectPath,
...this.form,
timezone: this.form.timezone.identifier,
},
},
})
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.createScheduleModal.hide();
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
setSelectedTimezone(tz) {
this.form.timezone = tz;
},
getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
},
hideErrorAlert() {
this.error = null;
},
},
};
</script>
<template>
<gl-modal
ref="createScheduleModal"
:modal-id="modalId"
size="sm"
:title="$options.i18n.addSchedule"
:action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel"
@primary.prevent="createSchedule"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<gl-form>
<gl-form-group
:label="$options.i18n.fields.name.title"
:invalid-feedback="$options.i18n.fields.name.validation.empty"
label-size="sm"
label-for="schedule-name"
>
<gl-form-input id="schedule-name" v-model="form.name" :state="!isNameInvalid" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.description.title"
label-size="sm"
label-for="schedule-description"
>
<gl-form-input id="schedule-description" v-model="form.description" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.timezone.title"
label-size="sm"
label-for="schedule-timezone"
:description="$options.i18n.fields.timezone.description"
:state="!isTimezoneInvalid"
:invalid-feedback="$options.i18n.fields.timezone.validation.empty"
>
<gl-dropdown
id="schedule-timezone"
:text="selectedTimezone"
class="timezone-dropdown gl-w-full"
:header-text="$options.i18n.selectTimezone"
:class="{ 'invalid-dropdown': isTimezoneInvalid }"
>
<gl-search-box-by-type v-model.trim="tzSearchTerm" />
<gl-dropdown-item
v-for="tz in filteredTimezones"
:key="getFormattedTimezone(tz)"
:is-checked="isTimezoneSelected(tz)"
is-check-item
@click="setSelectedTimezone(tz)"
>
<span class="gl-white-space-nowrap"> {{ getFormattedTimezone(tz) }}</span>
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults">
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</gl-form>
</gl-modal>
</template>
<script> <script>
import { GlEmptyState, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlButton, GlModalDirective } from '@gitlab/ui';
import AddScheduleModal from './add_schedule_modal.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
const addScheduleModalId = 'addScheduleModal';
export const i18n = { export const i18n = {
emptyState: { emptyState: {
title: s__('OnCallSchedules|Create on-call schedules in GitLab'), title: s__('OnCallSchedules|Create on-call schedules in GitLab'),
...@@ -12,27 +15,33 @@ export const i18n = { ...@@ -12,27 +15,33 @@ export const i18n = {
export default { export default {
i18n, i18n,
addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath'], inject: ['emptyOncallSchedulesSvgPath'],
components: { components: {
GlEmptyState, GlEmptyState,
GlButton, GlButton,
AddScheduleModal,
}, },
methods: { directives: {
createSchedule() {}, GlModal: GlModalDirective,
}, },
methods: {},
}; };
</script> </script>
<template> <template>
<gl-empty-state <div>
:title="$options.i18n.emptyState.title" <gl-empty-state
:description="$options.i18n.emptyState.description" :title="$options.i18n.emptyState.title"
:svg-path="emptyOncallSchedulesSvgPath" :description="$options.i18n.emptyState.description"
> :svg-path="emptyOncallSchedulesSvgPath"
<template #actions> >
<gl-button variant="info" @click="createSchedule">{{ <template #actions>
$options.i18n.emptyState.button <gl-button v-gl-modal="$options.addScheduleModalId" variant="info">
}}</gl-button> {{ $options.i18n.emptyState.button }}
</template> </gl-button>
</gl-empty-state> </template>
</gl-empty-state>
<add-schedule-modal :modal-id="$options.addScheduleModalId" />
</div>
</template> </template>
mutation oncallScheduleCreate($oncallScheduleCreateInput: OncallScheduleCreateInput!) {
oncallScheduleCreate(input: $oncallScheduleCreateInput) {
errors
oncallSchedule {
iid
name
description
timezone
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue'; import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default () => { export default () => {
const el = document.querySelector('#js-oncall_schedule'); const el = document.querySelector('#js-oncall_schedule');
if (!el) return null; if (!el) return null;
const { emptyOncallSchedulesSvgPath } = el.dataset; const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({ return new Vue({
el, el,
apolloProvider,
provide: { provide: {
projectPath,
emptyOncallSchedulesSvgPath, emptyOncallSchedulesSvgPath,
timezones: JSON.parse(timezones),
}, },
render(createElement) { render(createElement) {
return createElement(OnCallSchedulesWrapper); return createElement(OnCallSchedulesWrapper);
......
import Vue from 'vue'; import Vue from 'vue';
import 'ee/registrations/welcome/other_role';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { import {
STEPS, STEPS,
......
const role = document.querySelector('.js-user-role-dropdown');
const otherRoleGroup = document.querySelector('.js-other-role-group');
role.addEventListener('change', () => {
const enableOtherRole = role.value === 'other';
otherRoleGroup.classList.toggle('hidden', !enableOtherRole);
});
role.dispatchEvent(new Event('change'));
...@@ -17,7 +17,13 @@ export default { ...@@ -17,7 +17,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['totalAmount', 'name', 'usersPresent']), ...mapGetters([
'totalAmount',
'name',
'usersPresent',
'isGroupSelected',
'isSelectedGroupPresent',
]),
titleWithName() { titleWithName() {
return sprintf(this.$options.i18n.title, { name: this.name }); return sprintf(this.$options.i18n.title, { name: this.name });
}, },
...@@ -33,7 +39,10 @@ export default { ...@@ -33,7 +39,10 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="order-summary d-flex flex-column flex-grow-1 gl-mt-2 mt-lg-5"> <div
v-if="!isGroupSelected || isSelectedGroupPresent"
class="order-summary gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-mt-2 mt-lg-5"
>
<div class="d-lg-none"> <div class="d-lg-none">
<div @click="toggleCollapse"> <div @click="toggleCollapse">
<h4 class="d-flex justify-content-between gl-font-lg" :class="{ 'gl-mb-7': !collapsed }"> <h4 class="d-flex justify-content-between gl-font-lg" :class="{ 'gl-mb-7': !collapsed }">
......
...@@ -45,9 +45,14 @@ export const vat = (state, getters) => state.taxRate * getters.totalExVat; ...@@ -45,9 +45,14 @@ export const vat = (state, getters) => state.taxRate * getters.totalExVat;
export const totalAmount = (_, getters) => getters.totalExVat + getters.vat; export const totalAmount = (_, getters) => getters.totalExVat + getters.vat;
export const name = (state, getters) => { export const name = (state, getters) => {
if (state.isSetupForCompany && state.organizationName) return state.organizationName; if (state.isSetupForCompany && state.organizationName) {
else if (getters.isGroupSelected) return getters.selectedGroupName; return state.organizationName;
else if (state.isSetupForCompany) return s__('Checkout|Your organization'); } else if (getters.isGroupSelected && getters.isSelectedGroupPresent) {
return getters.selectedGroupName;
} else if (state.isSetupForCompany) {
return s__('Checkout|Your organization');
}
return state.fullName; return state.fullName;
}; };
...@@ -56,9 +61,20 @@ export const usersPresent = state => state.numberOfUsers > 0; ...@@ -56,9 +61,20 @@ export const usersPresent = state => state.numberOfUsers > 0;
export const isGroupSelected = state => export const isGroupSelected = state =>
state.selectedGroup !== null && state.selectedGroup !== NEW_GROUP; state.selectedGroup !== null && state.selectedGroup !== NEW_GROUP;
export const isSelectedGroupPresent = (state, getters) => {
return (
getters.isGroupSelected && state.groupData.some(group => group.value === state.selectedGroup)
);
};
export const selectedGroupUsers = (state, getters) => { export const selectedGroupUsers = (state, getters) => {
if (!getters.isGroupSelected) return 1; if (!getters.isGroupSelected) {
return state.groupData.find(group => group.value === state.selectedGroup).numberOfUsers; return 1;
} else if (getters.isSelectedGroupPresent) {
return state.groupData.find(group => group.value === state.selectedGroup).numberOfUsers;
}
return null;
}; };
export const selectedGroupName = (state, getters) => { export const selectedGroupName = (state, getters) => {
......
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
module IncidentManagement module IncidentManagement
module OncallScheduleHelper module OncallScheduleHelper
def oncall_schedule_data def oncall_schedule_data(project)
{ {
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg') 'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => timezone_data.to_json
} }
end end
end end
......
- page_title _('On-call schedules') - page_title _('On-call schedules')
- add_page_specific_style 'page_bundles/oncall_schedules'
#js-oncall_schedule{ data: oncall_schedule_data } #js-oncall_schedule{ data: oncall_schedule_data(@project) }
---
title: Fix the user experience when the user is unauthorized or trying to subscribe
for a non-existing group
merge_request: 48626
author:
type: fixed
...@@ -75,6 +75,71 @@ RSpec.describe 'Signup on EE' do ...@@ -75,6 +75,71 @@ RSpec.describe 'Signup on EE' do
end end
end end
context 'when the user_other_role_details feature flag is disabled' do
before do
stub_feature_flags(user_other_role_details: false)
end
context 'collects no collect a job title' do
it 'proceeds to the next step without collecting other_role' do
fill_in_signup_form
click_button "Register"
select 'Other', from: 'user_role'
expect(page).not_to have_field('What is your job title? (optional)')
choose 'user_setup_for_company_false'
click_button 'Get started!'
user = User.find_by_username!(new_user[:username])
expect(user.other_role).to be_blank
end
end
end
context 'when the user selects existing role' do
let_it_be(:job_title) { 'Guardian of the galaxy' }
it 'has the job title box' do
expect(page).not_to have_field('What is your job title? (optional)')
end
it 'proceeds to the next step' do
fill_in_signup_form
click_button "Register"
select 'Software Developer', from: 'user_role'
choose 'user_setup_for_company_false'
click_button 'Get started!'
user = User.find_by_username!(new_user[:username])
expect(user.other_role).to be_blank
end
end
context 'when the user selects other role' do
let_it_be(:job_title) { 'Guardian of the galaxy' }
it 'has the job title box' do
expect(page).not_to have_field('What is your job title? (optional)')
end
it 'proceeds to the next step' do
fill_in_signup_form
click_button "Register"
select 'Other', from: 'user_role'
expect(page).to have_field('What is your job title? (optional)')
choose 'user_setup_for_company_false'
fill_in 'What is your job title? (optional)', with: job_title
click_button 'Get started!'
user = User.find_by_username!(new_user[:username])
expect(user.other_role).to eq(job_title)
end
end
it 'redirects to step 2 of the signup process, sets the role and setup for company and redirects back' do it 'redirects to step 2 of the signup process, sets the role and setup for company and redirects back' do
fill_in_signup_form fill_in_signup_form
click_button 'Register' click_button 'Register'
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddScheduleModal renders modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="modalId"
size="sm"
title="Add schedule"
titletag="h4"
>
<!---->
<gl-form-stub>
<gl-form-group-stub
invalid-feedback="Can't be empty"
label="Name"
label-for="schedule-name"
label-size="sm"
>
<gl-form-input-stub
id="schedule-name"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
label="Description (optional)"
label-for="schedule-description"
label-size="sm"
>
<gl-form-input-stub
id="schedule-description"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
description="Sets the default timezone for the schedule, for all participants"
invalid-feedback="Can't be empty"
label="Timezone"
label-for="schedule-timezone"
label-size="sm"
>
<gl-dropdown-stub
category="primary"
class="timezone-dropdown gl-w-full invalid-dropdown"
headertext="Select timezone"
id="schedule-timezone"
size="medium"
text="Select timezone"
variant="default"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
value=""
/>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-12:00) -12 International Date Line West
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST American Samoa
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST Midway Island
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-10:00) HST Hawaii
</span>
</gl-dropdown-item-stub>
<!---->
</gl-dropdown-stub>
</gl-form-group-stub>
</gl-form-stub>
</gl-modal-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { GlSearchBoxByType, GlDropdown, GlDropdownItem, GlModal, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal, { i18n } from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import mockTimezones from './mocks/mockTimezones.json';
describe('AddScheduleModal', () => {
let wrapper;
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
function mountComponent() {
wrapper = shallowMount(AddScheduleModal, {
propsData: {
modalId: 'modalId',
},
provide: {
projectPath,
timezones: mockTimezones,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: {
GlFormGroup: false,
},
});
wrapper.vm.$refs.createScheduleModal.hide = mockHideModal;
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findTimezoneDropdown = () => wrapper.find(GlDropdown);
const findDropdownOptions = () => wrapper.findAll(GlDropdownItem);
const findTimezoneSearchBox = () => wrapper.find(GlSearchBoxByType);
it('renders modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('Timezone select', () => {
it('has options based on provided BE data', () => {
expect(findDropdownOptions().length).toBe(mockTimezones.length);
});
it('formats each option', () => {
findDropdownOptions().wrappers.forEach((option, index) => {
const tz = mockTimezones[index];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(option.text()).toBe(expectedValue);
});
});
describe('timezones filtering', () => {
it('should filter options based on search term', async () => {
const searchTerm = 'Hawaii';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(searchTerm);
});
it('should display no results item when there are no filter matches', async () => {
const searchTerm = 'someUnexistentTZ';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(i18n.noResults);
});
});
it('should add a checkmark to the selected option', async () => {
const selectedTZOption = findDropdownOptions().at(0);
selectedTZOption.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(selectedTZOption.attributes('ischecked')).toBe('true');
});
});
describe('Schedule create', () => {
it('makes a request with form data to create a schedule', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
variables: { oncallScheduleCreateInput: expect.objectContaining({ projectPath }) },
});
});
it('hides the modal on successful schedule creation', async () => {
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide a modal and shows error alert on fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
});
describe('Form validation', () => {
describe('Timezone select', () => {
it('has red border when nothing selected', () => {
expect(findTimezoneDropdown().classes()).toContain('invalid-dropdown');
});
it("doesn't have a red border when there is selected opeion", async () => {
findDropdownOptions()
.at(1)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findTimezoneDropdown().classes()).not.toContain('invalid-dropdown');
});
});
});
});
[
{
"identifier": "Etc/GMT+12",
"name": "International Date Line West",
"abbr": "-12",
"formatted_offset": "-12:00"
},
{
"identifier": "Pacific/Pago_Pago",
"name": "American Samoa",
"abbr": "SST",
"formatted_offset": "-11:00"
},
{
"identifier": "Pacific/Midway",
"name": "Midway Island",
"abbr": "SST",
"formatted_offset": "-11:00"
},
{
"identifier": "Pacific/Honolulu",
"name": "Hawaii",
"abbr": "HST",
"formatted_offset": "-10:00"
}
]
...@@ -105,11 +105,28 @@ describe('Subscriptions Getters', () => { ...@@ -105,11 +105,28 @@ describe('Subscriptions Getters', () => {
).toBe('My organization'); ).toBe('My organization');
}); });
it('returns the organization name when a group is selected but does not exist', () => {
expect(
getters.name(
{ isSetupForCompany: true },
{
isGroupSelected: true,
isSelectedGroupPresent: false,
selectedGroupName: 'Selected group',
},
),
).toBe('Your organization');
});
it('returns the selected group name a group is selected', () => { it('returns the selected group name a group is selected', () => {
expect( expect(
getters.name( getters.name(
{ isSetupForCompany: true }, { isSetupForCompany: true },
{ isGroupSelected: true, selectedGroupName: 'Selected group' }, {
isGroupSelected: true,
isSelectedGroupPresent: true,
selectedGroupName: 'Selected group',
},
), ),
).toBe('Selected group'); ).toBe('Selected group');
}); });
...@@ -161,16 +178,54 @@ describe('Subscriptions Getters', () => { ...@@ -161,16 +178,54 @@ describe('Subscriptions Getters', () => {
).toBe(1); ).toBe(1);
}); });
it('returns `null` when a group is selected, but not present', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true, isSelectedGroupPresent: false },
),
).toBe(null);
});
it('returns the number of users of the selected group when a group is selected', () => { it('returns the number of users of the selected group when a group is selected', () => {
expect( expect(
getters.selectedGroupUsers( getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 }, { groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true }, { isGroupSelected: true, isSelectedGroupPresent: true },
), ),
).toBe(3); ).toBe(3);
}); });
}); });
describe('isSelectedGroupPresent', () => {
it('returns false when group is not selected', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: null },
{ isGroupSelected: false },
),
).toBe(false);
});
it('returns false when group is selected, but not present', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 321 },
{ isGroupSelected: true },
),
).toBe(false);
});
it('returns true when group is selected and is present', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true },
),
).toBe(true);
});
});
describe('selectedGroupName', () => { describe('selectedGroupName', () => {
it('returns null when no group is selected', () => { it('returns null when no group is selected', () => {
expect( expect(
......
...@@ -6,11 +6,13 @@ RSpec.describe IncidentManagement::OncallScheduleHelper do ...@@ -6,11 +6,13 @@ RSpec.describe IncidentManagement::OncallScheduleHelper do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
describe '#oncall_schedule_data' do describe '#oncall_schedule_data' do
subject(:data) { helper.oncall_schedule_data } subject(:data) { helper.oncall_schedule_data(project) }
it 'returns on-call schedule data' do it 'returns on-call schedule data' do
is_expected.to eq( is_expected.to eq(
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg') 'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => helper.timezone_data.to_json
) )
end end
end end
......
...@@ -6,12 +6,14 @@ RSpec.describe 'registrations/welcome/show' do ...@@ -6,12 +6,14 @@ RSpec.describe 'registrations/welcome/show' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:user) { User.new } let_it_be(:user) { User.new }
let_it_be(:user_other_role_details_enabled) { false }
before do before do
allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:redirect_path).and_return(redirect_path) allow(view).to receive(:redirect_path).and_return(redirect_path)
allow(view).to receive(:onboarding_issues_experiment_enabled?).and_return(onboarding_issues_experiment_enabled) allow(view).to receive(:onboarding_issues_experiment_enabled?).and_return(onboarding_issues_experiment_enabled)
allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
stub_feature_flags(user_other_role_details: user_other_role_details_enabled)
render render
end end
...@@ -49,5 +51,14 @@ RSpec.describe 'registrations/welcome/show' do ...@@ -49,5 +51,14 @@ RSpec.describe 'registrations/welcome/show' do
else else
it { is_expected.not_to have_selector('#progress-bar') } it { is_expected.not_to have_selector('#progress-bar') }
end end
context 'feature flag other_role_details is enabled' do
let_it_be(:user_other_role_details_enabled) { true }
it 'has a text field for other role' do
is_expected.not_to have_selector('input[type="hidden"][name="user[other_role]"]', visible: false)
is_expected.to have_selector('input[type="text"][name="user[other_role]"]')
end
end
end end
end end
...@@ -79,10 +79,9 @@ module Gitlab ...@@ -79,10 +79,9 @@ module Gitlab
end end
def wiki_restorer def wiki_restorer
Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path, Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: shared, shared: shared,
project: ProjectWiki.new(project), project: ProjectWiki.new(project))
wiki_enabled: project.wiki_enabled?)
end end
def design_repo_restorer def design_repo_restorer
......
# frozen_string_literal: true
module Gitlab
module ImportExport
class WikiRestorer < RepoRestorer
def initialize(project:, shared:, path_to_bundle:, wiki_enabled:)
super(project: project, shared: shared, path_to_bundle: path_to_bundle)
@project = project
@wiki_enabled = wiki_enabled
end
def restore
project.wiki if create_empty_wiki?
super
end
private
attr_accessor :project, :wiki_enabled
def create_empty_wiki?
!File.exist?(path_to_bundle) && wiki_enabled
end
end
end
end
...@@ -4881,6 +4881,9 @@ msgstr "" ...@@ -4881,6 +4881,9 @@ msgstr ""
msgid "Can't apply this suggestion." msgid "Can't apply this suggestion."
msgstr "" msgstr ""
msgid "Can't be empty"
msgstr ""
msgid "Can't create snippet: %{err}" msgid "Can't create snippet: %{err}"
msgstr "" msgstr ""
...@@ -9348,6 +9351,9 @@ msgstr "" ...@@ -9348,6 +9351,9 @@ msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
msgid "Description (optional)"
msgstr ""
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}" msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
msgstr "" msgstr ""
...@@ -19033,12 +19039,24 @@ msgstr "" ...@@ -19033,12 +19039,24 @@ msgstr ""
msgid "OnCallSchedules|Add a schedule" msgid "OnCallSchedules|Add a schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|Add schedule"
msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab" msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr "" msgstr ""
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
msgid "OnCallSchedules|Route alerts directly to specific members of your team" msgid "OnCallSchedules|Route alerts directly to specific members of your team"
msgstr "" msgstr ""
msgid "OnCallSchedules|Select timezone"
msgstr ""
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr "" msgstr ""
...@@ -28436,6 +28454,9 @@ msgstr "" ...@@ -28436,6 +28454,9 @@ msgstr ""
msgid "Timeout connecting to the Google API. Please try again." msgid "Timeout connecting to the Google API. Please try again."
msgstr "" msgstr ""
msgid "Timezone"
msgstr ""
msgid "Time|hr" msgid "Time|hr"
msgid_plural "Time|hrs" msgid_plural "Time|hrs"
msgstr[0] "" msgstr[0] ""
...@@ -30649,6 +30670,9 @@ msgstr "" ...@@ -30649,6 +30670,9 @@ msgstr ""
msgid "What is squashing?" msgid "What is squashing?"
msgstr "" msgstr ""
msgid "What is your job title? (optional)"
msgstr ""
msgid "What's new at GitLab" msgid "What's new at GitLab"
msgstr "" msgstr ""
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Freeze Periods (JavaScript fixtures)' do RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
include JavaScriptFixturesHelpers include JavaScriptFixturesHelpers
include Ci::PipelineSchedulesHelper include TimeZoneHelper
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') } let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
...@@ -40,10 +40,12 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do ...@@ -40,10 +40,12 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
end end
end end
describe Ci::PipelineSchedulesHelper, '(JavaScript fixtures)' do describe TimeZoneHelper, '(JavaScript fixtures)' do
let(:response) { timezone_data.to_json } let(:response) { timezone_data.to_json }
it 'api/freeze-periods/timezone_data.json' do it 'api/freeze-periods/timezone_data.json' do
# Looks empty but does things
# More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38525/diffs#note_391048415
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PipelineSchedulesHelper, :aggregate_failures do
describe '#timezone_data' do
subject { helper.timezone_data }
it 'matches schema' do
expect(subject).not_to be_empty
subject.each_with_index do |timzone_hash, i|
expect(timzone_hash.keys).to contain_exactly(:name, :offset, :identifier), "Failed at index #{i}"
end
end
it 'formats for display' do
first_timezone = ActiveSupport::TimeZone.all[0]
expect(subject[0][:name]).to eq(first_timezone.name)
expect(subject[0][:offset]).to eq(first_timezone.now.utc_offset)
expect(subject[0][:identifier]).to eq(first_timezone.tzinfo.identifier)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TimeZoneHelper, :aggregate_failures do
describe '#timezone_data' do
subject(:timezone_data) { helper.timezone_data }
it 'matches schema' do
expect(timezone_data).not_to be_empty
timezone_data.each_with_index do |timezone_hash, i|
expect(timezone_hash.keys).to contain_exactly(
:identifier,
:name,
:abbr,
:offset,
:formatted_offset
), "Failed at index #{i}"
end
end
it 'formats for display' do
tz = ActiveSupport::TimeZone.all[0]
expect(timezone_data[0]).to eq(
identifier: tz.tzinfo.identifier,
name: tz.name,
abbr: tz.tzinfo.strftime('%Z'),
offset: tz.now.utc_offset,
formatted_offset: tz.now.formatted_offset
)
end
end
end
...@@ -48,7 +48,6 @@ RSpec.describe Gitlab::ImportExport::Importer do ...@@ -48,7 +48,6 @@ RSpec.describe Gitlab::ImportExport::Importer do
[ [
Gitlab::ImportExport::AvatarRestorer, Gitlab::ImportExport::AvatarRestorer,
Gitlab::ImportExport::RepoRestorer, Gitlab::ImportExport::RepoRestorer,
Gitlab::ImportExport::WikiRestorer,
Gitlab::ImportExport::UploadsRestorer, Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer, Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer, Gitlab::ImportExport::StatisticsRestorer,
...@@ -65,6 +64,20 @@ RSpec.describe Gitlab::ImportExport::Importer do ...@@ -65,6 +64,20 @@ RSpec.describe Gitlab::ImportExport::Importer do
end end
end end
it 'calls RepoRestorer with project and wiki' do
wiki_repo_path = File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename)
repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename)
restorer = double(Gitlab::ImportExport::RepoRestorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original
expect(restorer).to receive(:restore).and_return(true).twice
importer.execute
end
context 'with sample_data_template' do context 'with sample_data_template' do
it 'initializes the Sample::TreeRestorer' do it 'initializes the Sample::TreeRestorer' do
project.create_or_update_import_data(data: { sample_data: true }) project.create_or_update_import_data(data: { sample_data: true })
......
...@@ -5,35 +5,42 @@ require 'spec_helper' ...@@ -5,35 +5,42 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RepoRestorer do RSpec.describe Gitlab::ImportExport::RepoRestorer do
include GitHelpers include GitHelpers
let_it_be(:project_with_repo) do
create(:project, :repository, :wiki_repo, name: 'test-repo-restorer', path: 'test-repo-restorer').tap do |p|
p.wiki.create_page('page', 'foobar', :markdown, 'created page')
end
end
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
bundler.save
end
after do
FileUtils.rm_rf(export_path)
end
describe 'bundle a project Git repo' do describe 'bundle a project Git repo' do
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) } subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) }
before do
allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(export_path)
end
bundler.save
end
after do after do
FileUtils.rm_rf(export_path) Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
FileUtils.rm_rf(project.repository.path_to_repo)
end
end end
it 'restores the repo successfully' do it 'restores the repo successfully' do
expect(project.repository.exists?).to be false
expect(subject.restore).to be_truthy expect(subject.restore).to be_truthy
expect(project.repository.empty?).to be false
end end
context 'when the repository already exists' do context 'when the repository already exists' do
...@@ -53,4 +60,35 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do ...@@ -53,4 +60,35 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end end
end end
end end
describe 'restore a wiki Git repo' do
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) }
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) }
after do
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
it 'restores the wiki repo successfully' do
expect(project.wiki_repository_exists?).to be false
subject.restore
project.wiki.repository.expire_status_cache
expect(project.wiki_repository_exists?).to be true
end
describe 'no wiki in the bundle' do
let!(:project_without_wiki) { create(:project) }
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
it 'does not creates an empty wiki' do
expect(subject.restore).to be true
expect(project.wiki_repository_exists?).to be false
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::WikiRestorer do
describe 'restore a wiki Git repo' do
let!(:project_with_wiki) { create(:project, :wiki_repo) }
let!(:project_without_wiki) { create(:project) }
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:restorer) do
described_class.new(path_to_bundle: bundle_path,
shared: shared,
project: project.wiki,
wiki_enabled: true)
end
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
bundler.save
end
after do
FileUtils.rm_rf(export_path)
Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path)
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
it 'restores the wiki repo successfully' do
expect(restorer.restore).to be true
end
describe "no wiki in the bundle" do
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
it 'creates an empty wiki' do
expect(restorer.restore).to be true
expect(project.wiki_repository_exists?).to be true
end
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