Commit 09d16635 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 19ec5459 51f59fb5
...@@ -870,6 +870,14 @@ const Api = { ...@@ -870,6 +870,14 @@ const Api = {
return axios.put(url, freezePeriod); return axios.put(url, freezePeriod);
}, },
deleteFreezePeriod(id, freezePeriodId) {
const url = Api.buildUrl(this.freezePeriodPath)
.replace(':id', encodeURIComponent(id))
.replace(':freeze_period_id', encodeURIComponent(freezePeriodId));
return axios.delete(url);
},
trackRedisCounterEvent(event) { trackRedisCounterEvent(event) {
if (!gon.features?.usageDataApi) { if (!gon.features?.usageDataApi) {
return null; return null;
......
<script> <script>
import { GlTable, GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui'; import { GlTable, GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -21,21 +21,42 @@ export default { ...@@ -21,21 +21,42 @@ export default {
key: 'edit', key: 'edit',
label: s__('DeployFreeze|Edit'), label: s__('DeployFreeze|Edit'),
}, },
{
key: 'delete',
label: s__('DeployFreeze|Delete'),
},
], ],
translations: { translations: {
addDeployFreeze: s__('DeployFreeze|Add deploy freeze'), addDeployFreeze: s__('DeployFreeze|Add deploy freeze'),
deleteDeployFreezeTitle: s__('DeployFreeze|Delete deploy freeze?'),
deleteDeployFreezeMessage: s__(
'DeployFreeze|Deploy freeze from %{start} to %{end} in %{timezone} will be removed. Are you sure?',
),
emptyStateText: s__( emptyStateText: s__(
'DeployFreeze|No deploy freezes exist for this project. To add one, select %{strongStart}Add deploy freeze%{strongEnd}', 'DeployFreeze|No deploy freezes exist for this project. To add one, select %{strongStart}Add deploy freeze%{strongEnd}',
), ),
}, },
modal: {
id: 'deleteFreezePeriodModal',
actionPrimary: {
text: s__('DeployFreeze|Delete freeze period'),
attributes: { variant: 'danger', 'data-testid': 'modal-confirm' },
},
},
components: { components: {
GlTable, GlTable,
GlButton, GlButton,
GlModal,
GlSprintf, GlSprintf,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
}, },
data() {
return {
freezePeriodToDelete: null,
};
},
computed: { computed: {
...mapState(['freezePeriods']), ...mapState(['freezePeriods']),
tableIsNotEmpty() { tableIsNotEmpty() {
...@@ -46,7 +67,14 @@ export default { ...@@ -46,7 +67,14 @@ export default {
this.fetchFreezePeriods(); this.fetchFreezePeriods();
}, },
methods: { methods: {
...mapActions(['fetchFreezePeriods', 'setFreezePeriod']), ...mapActions(['fetchFreezePeriods', 'setFreezePeriod', 'deleteFreezePeriod']),
handleDeleteFreezePeriod(freezePeriod) {
this.freezePeriodToDelete = freezePeriod;
},
confirmDeleteFreezePeriod() {
this.deleteFreezePeriod(this.freezePeriodToDelete);
this.freezePeriodToDelete = null;
},
}, },
}; };
</script> </script>
...@@ -72,6 +100,18 @@ export default { ...@@ -72,6 +100,18 @@ export default {
@click="setFreezePeriod(item)" @click="setFreezePeriod(item)"
/> />
</template> </template>
<template #cell(delete)="{ item }">
<gl-button
v-gl-modal="$options.modal.id"
category="secondary"
variant="danger"
icon="remove"
:aria-label="$options.modal.actionPrimary.text"
:loading="item.isDeleting"
data-testid="delete-deploy-freeze"
@click="handleDeleteFreezePeriod(item)"
/>
</template>
<template #empty> <template #empty>
<p data-testid="empty-freeze-periods" class="gl-text-center text-plain"> <p data-testid="empty-freeze-periods" class="gl-text-center text-plain">
<gl-sprintf :message="$options.translations.emptyStateText"> <gl-sprintf :message="$options.translations.emptyStateText">
...@@ -90,5 +130,24 @@ export default { ...@@ -90,5 +130,24 @@ export default {
> >
{{ $options.translations.addDeployFreeze }} {{ $options.translations.addDeployFreeze }}
</gl-button> </gl-button>
<gl-modal
:title="$options.translations.deleteDeployFreezeTitle"
:modal-id="$options.modal.id"
:action-primary="$options.modal.actionPrimary"
static
@primary="confirmDeleteFreezePeriod"
>
<template v-if="freezePeriodToDelete">
<gl-sprintf :message="$options.translations.deleteDeployFreezeMessage">
<template #start>
<code>{{ freezePeriodToDelete.freezeStart }}</code>
</template>
<template #end>
<code>{{ freezePeriodToDelete.freezeEnd }}</code>
</template>
<template #timezone>{{ freezePeriodToDelete.cronTimezone.formattedTimezone }}</template>
</gl-sprintf>
</template>
</gl-modal>
</div> </div>
</template> </template>
...@@ -52,6 +52,22 @@ export const updateFreezePeriod = (store) => ...@@ -52,6 +52,22 @@ export const updateFreezePeriod = (store) =>
}), }),
); );
export const deleteFreezePeriod = ({ state, commit }, { id }) => {
commit(types.REQUEST_DELETE_FREEZE_PERIOD, id);
return Api.deleteFreezePeriod(state.projectId, id)
.then(() => commit(types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS, id))
.catch((e) => {
createFlash({
message: __('Error: Unable to delete deploy freeze'),
});
commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
// eslint-disable-next-line no-console
console.error('[gitlab] Unable to delete deploy freeze:', e);
});
};
export const fetchFreezePeriods = ({ commit, state }) => { export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.REQUEST_FREEZE_PERIODS); commit(types.REQUEST_FREEZE_PERIODS);
......
...@@ -10,4 +10,8 @@ export const SET_SELECTED_ID = 'SET_SELECTED_ID'; ...@@ -10,4 +10,8 @@ export const SET_SELECTED_ID = 'SET_SELECTED_ID';
export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON'; export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON';
export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON'; export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON';
export const REQUEST_DELETE_FREEZE_PERIOD = 'REQUEST_DELETE_FREEZE_PERIOD';
export const RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS = 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS';
export const RECEIVE_DELETE_FREEZE_PERIOD_ERROR = 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR';
export const RESET_MODAL = 'RESET_MODAL'; export const RESET_MODAL = 'RESET_MODAL';
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { secondsToHours } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
const formatTimezoneName = (freezePeriod, timezoneList) => const formatTimezoneName = (freezePeriod, timezoneList) => {
convertObjectPropsToCamelCase({ const tz = timezoneList.find((timezone) => timezone.identifier === freezePeriod.cron_timezone);
return convertObjectPropsToCamelCase({
...freezePeriod, ...freezePeriod,
cron_timezone: { cron_timezone: {
formattedTimezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone) formattedTimezone: tz && `[UTC ${secondsToHours(tz.offset)}] ${tz.name}`,
?.name,
identifier: freezePeriod.cron_timezone, identifier: freezePeriod.cron_timezone,
}, },
}); });
};
const setFreezePeriodIsDeleting = (state, id, isDeleting) => {
const freezePeriod = state.freezePeriods.find((f) => f.id === id);
if (!freezePeriod) {
return;
}
Vue.set(freezePeriod, 'isDeleting', isDeleting);
};
export default { export default {
[types.REQUEST_FREEZE_PERIODS](state) { [types.REQUEST_FREEZE_PERIODS](state) {
...@@ -53,6 +66,18 @@ export default { ...@@ -53,6 +66,18 @@ export default {
state.selectedId = id; state.selectedId = id;
}, },
[types.REQUEST_DELETE_FREEZE_PERIOD](state, id) {
setFreezePeriodIsDeleting(state, id, true);
},
[types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS](state, id) {
state.freezePeriods = state.freezePeriods.filter((f) => f.id !== id);
},
[types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR](state, id) {
setFreezePeriodIsDeleting(state, id, false);
},
[types.RESET_MODAL](state) { [types.RESET_MODAL](state) {
state.freezeStartCron = ''; state.freezeStartCron = '';
state.freezeEndCron = ''; state.freezeEndCron = '';
......
...@@ -21,9 +21,10 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -21,9 +21,10 @@ class RegistrationsController < Devise::RegistrationsController
def create def create
set_user_state set_user_state
accept_pending_invitations
super do |new_user| super do |new_user|
accept_pending_invitations if new_user.persisted?
persist_accepted_terms_if_required(new_user) persist_accepted_terms_if_required(new_user)
set_role_required(new_user) set_role_required(new_user)
......
...@@ -52,15 +52,22 @@ module Types ...@@ -52,15 +52,22 @@ module Types
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def jobs_for_pipeline(pipeline, stage_ids, include_needs) def jobs_for_pipeline(pipeline, stage_ids, include_needs)
builds_results = pipeline.latest_builds.where(stage_id: stage_ids).preload(:job_artifacts, :project) jobs = pipeline.statuses.latest.where(stage_id: stage_ids)
bridges_results = pipeline.bridges.where(stage_id: stage_ids).preload(:project)
builds_results = builds_results.preload(:needs) if include_needs
bridges_results = bridges_results.preload(:needs) if include_needs
commit_status_results = pipeline.latest_statuses.where(stage_id: stage_ids)
results = builds_results | bridges_results | commit_status_results common_relations = [:project]
common_relations << :needs if include_needs
results.group_by(&:stage_id) preloaders = {
::Ci::Build => [:metadata, :job_artifacts],
::Ci::Bridge => [:metadata, :downstream_pipeline],
::GenericCommitStatus => []
}
preloaders.each do |klass, relations|
ActiveRecord::Associations::Preloader.new.preload(jobs.select { |job| job.is_a?(klass) }, relations + common_relations)
end
jobs.group_by(&:stage_id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
......
...@@ -66,6 +66,7 @@ module Ci ...@@ -66,6 +66,7 @@ module Ci
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :generic_commit_statuses, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'GenericCommitStatus'
has_many :job_artifacts, through: :builds has_many :job_artifacts, through: :builds
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable' has_many :variables, class_name: 'Ci::PipelineVariable'
......
...@@ -278,12 +278,14 @@ class Member < ApplicationRecord ...@@ -278,12 +278,14 @@ class Member < ApplicationRecord
def accept_invite!(new_user) def accept_invite!(new_user)
return false unless invite? return false unless invite?
return false unless new_user
self.user = new_user
return false unless self.user.save
self.invite_token = nil self.invite_token = nil
self.invite_accepted_at = Time.current.utc self.invite_accepted_at = Time.current.utc
self.user = new_user
saved = self.save saved = self.save
after_accept_invite if saved after_accept_invite if saved
......
...@@ -582,7 +582,7 @@ class Note < ApplicationRecord ...@@ -582,7 +582,7 @@ class Note < ApplicationRecord
end end
def post_processed_cache_key def post_processed_cache_key
cache_key_items = [cache_key, author.cache_key] cache_key_items = [cache_key, author&.cache_key]
cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present? cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
cache_key_items.join(':') cache_key_items.join(':')
......
...@@ -186,7 +186,8 @@ To subscribe to notifications for releases: ...@@ -186,7 +186,8 @@ To subscribe to notifications for releases:
## Prevent unintentional releases by setting a deploy freeze ## Prevent unintentional releases by setting a deploy freeze
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
> - The ability to delete freeze periods through the UI was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212451) in GitLab 14.3.
Prevent unintended production releases during a period of time you specify by Prevent unintended production releases during a period of time you specify by
setting a [*deploy freeze* period](../../../ci/environments/deployment_safety.md). setting a [*deploy freeze* period](../../../ci/environments/deployment_safety.md).
...@@ -219,11 +220,8 @@ To set a deploy freeze window in the UI, complete these steps: ...@@ -219,11 +220,8 @@ To set a deploy freeze window in the UI, complete these steps:
1. Click **Add deploy freeze** to open the deploy freeze modal. 1. Click **Add deploy freeze** to open the deploy freeze modal.
1. Enter the start time, end time, and timezone of the desired deploy freeze period. 1. Enter the start time, end time, and timezone of the desired deploy freeze period.
1. Click **Add deploy freeze** in the modal. 1. Click **Add deploy freeze** in the modal.
1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**). 1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**) and remove it by selecting the delete button (**{remove}**).
![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v13_10.png) ![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v14_3.png)
WARNING:
To delete a deploy freeze, use the [Freeze Periods API](../../../api/freeze_periods.md).
If a project contains multiple freeze periods, all periods apply. If they overlap, the freeze covers the If a project contains multiple freeze periods, all periods apply. If they overlap, the freeze covers the
complete overlapping period. complete overlapping period.
......
...@@ -11068,6 +11068,18 @@ msgstr "" ...@@ -11068,6 +11068,18 @@ msgstr ""
msgid "DeployFreeze|Add deploy freeze" msgid "DeployFreeze|Add deploy freeze"
msgstr "" msgstr ""
msgid "DeployFreeze|Delete"
msgstr ""
msgid "DeployFreeze|Delete deploy freeze?"
msgstr ""
msgid "DeployFreeze|Delete freeze period"
msgstr ""
msgid "DeployFreeze|Deploy freeze from %{start} to %{end} in %{timezone} will be removed. Are you sure?"
msgstr ""
msgid "DeployFreeze|Edit" msgid "DeployFreeze|Edit"
msgstr "" msgstr ""
...@@ -13262,6 +13274,9 @@ msgstr "" ...@@ -13262,6 +13274,9 @@ msgstr ""
msgid "Error: Unable to create deploy freeze" msgid "Error: Unable to create deploy freeze"
msgstr "" msgstr ""
msgid "Error: Unable to delete deploy freeze"
msgstr ""
msgid "Error: Unable to find AWS role for current user" msgid "Error: Unable to find AWS role for current user"
msgstr "" msgstr ""
......
...@@ -283,6 +283,26 @@ RSpec.describe RegistrationsController do ...@@ -283,6 +283,26 @@ RSpec.describe RegistrationsController do
end end
end end
context 'when the registration fails' do
let_it_be(:member) { create(:project_member, :invited) }
let_it_be(:missing_user_params) do
{ username: '', email: member.invite_email, password: 'Any_password' }
end
let_it_be(:user_params) { { user: missing_user_params } }
let(:session_params) { { invite_email: member.invite_email } }
subject { post(:create, params: user_params, session: session_params) }
it 'does not delete the invitation or register the new user' do
subject
expect(member.invite_token).not_to be_nil
expect(controller.current_user).to be_nil
end
end
context 'when soft email confirmation is enabled' do context 'when soft email confirmation is enabled' do
before do before do
stub_feature_flags(soft_email_confirmation: true) stub_feature_flags(soft_email_confirmation: true)
......
...@@ -189,6 +189,16 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do ...@@ -189,6 +189,16 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end end
context 'email confirmation enabled' do context 'email confirmation enabled' do
context 'when user is not valid in sign up form' do
let(:new_user) { build_stubbed(:user, first_name: '', last_name: '') }
it 'fails sign up and redirects back to sign up', :aggregate_failures do
expect { fill_in_sign_up_form(new_user) }.not_to change { User.count }
expect(page).to have_content('prohibited this user from being saved')
expect(current_path).to eq(user_registration_path)
end
end
context 'with invite email acceptance', :snowplow do context 'with invite email acceptance', :snowplow do
it 'tracks the accepted invite' do it 'tracks the accepted invite' do
fill_in_sign_up_form(new_user) fill_in_sign_up_form(new_user)
......
import { GlModal } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue'; import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
...@@ -29,6 +30,8 @@ describe('Deploy freeze table', () => { ...@@ -29,6 +30,8 @@ describe('Deploy freeze table', () => {
const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]'); const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]');
const findEditDeployFreezeButton = () => wrapper.find('[data-testid="edit-deploy-freeze"]'); const findEditDeployFreezeButton = () => wrapper.find('[data-testid="edit-deploy-freeze"]');
const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]'); const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]');
const findDeleteDeployFreezeButton = () => wrapper.find('[data-testid="delete-deploy-freeze"]');
const findDeleteDeployFreezeModal = () => wrapper.findComponent(GlModal);
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
...@@ -73,6 +76,29 @@ describe('Deploy freeze table', () => { ...@@ -73,6 +76,29 @@ describe('Deploy freeze table', () => {
store.state.freezePeriods[0], store.state.freezePeriods[0],
); );
}); });
it('displays delete deploy freeze button', () => {
expect(findDeleteDeployFreezeButton().exists()).toBe(true);
});
it('confirms a user wants to delete a deploy freeze', async () => {
const [{ freezeStart, freezeEnd, cronTimezone }] = store.state.freezePeriods;
await findDeleteDeployFreezeButton().trigger('click');
const modal = findDeleteDeployFreezeModal();
expect(modal.text()).toContain(
`Deploy freeze from ${freezeStart} to ${freezeEnd} in ${cronTimezone.formattedTimezone} will be removed.`,
);
});
it('deletes the freeze period on confirmation', async () => {
await findDeleteDeployFreezeButton().trigger('click');
const modal = findDeleteDeployFreezeModal();
modal.vm.$emit('primary');
expect(store.dispatch).toHaveBeenCalledWith(
'deleteFreezePeriod',
store.state.freezePeriods[0],
);
});
}); });
}); });
......
...@@ -12,6 +12,7 @@ jest.mock('~/api.js'); ...@@ -12,6 +12,7 @@ jest.mock('~/api.js');
jest.mock('~/flash.js'); jest.mock('~/flash.js');
describe('deploy freeze store actions', () => { describe('deploy freeze store actions', () => {
const freezePeriodFixture = freezePeriodsFixture[0];
let mock; let mock;
let state; let state;
...@@ -24,6 +25,7 @@ describe('deploy freeze store actions', () => { ...@@ -24,6 +25,7 @@ describe('deploy freeze store actions', () => {
Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture }); Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture });
Api.createFreezePeriod.mockResolvedValue(); Api.createFreezePeriod.mockResolvedValue();
Api.updateFreezePeriod.mockResolvedValue(); Api.updateFreezePeriod.mockResolvedValue();
Api.deleteFreezePeriod.mockResolvedValue();
}); });
afterEach(() => { afterEach(() => {
...@@ -195,4 +197,46 @@ describe('deploy freeze store actions', () => { ...@@ -195,4 +197,46 @@ describe('deploy freeze store actions', () => {
); );
}); });
}); });
describe('deleteFreezePeriod', () => {
it('dispatch correct actions on deleting a freeze period', () => {
testAction(
actions.deleteFreezePeriod,
freezePeriodFixture,
state,
[
{ type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS', payload: freezePeriodFixture.id },
],
[],
() =>
expect(Api.deleteFreezePeriod).toHaveBeenCalledWith(
state.projectId,
freezePeriodFixture.id,
),
);
});
it('should show flash error and set error in state on delete failure', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
const error = new Error();
Api.deleteFreezePeriod.mockRejectedValue(error);
testAction(
actions.deleteFreezePeriod,
freezePeriodFixture,
state,
[
{ type: 'REQUEST_DELETE_FREEZE_PERIOD', payload: freezePeriodFixture.id },
{ type: 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR', payload: freezePeriodFixture.id },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith('[gitlab] Unable to delete deploy freeze:', error);
},
);
});
});
}); });
...@@ -28,9 +28,9 @@ describe('Deploy freeze mutations', () => { ...@@ -28,9 +28,9 @@ describe('Deploy freeze mutations', () => {
describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => { describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
it('should set freeze periods and format timezones from identifiers to names', () => { it('should set freeze periods and format timezones from identifiers to names', () => {
const timezoneNames = { const timezoneNames = {
'Europe/Berlin': 'Berlin', 'Europe/Berlin': '[UTC 2] Berlin',
'Etc/UTC': 'UTC', 'Etc/UTC': '[UTC 0] UTC',
'America/New_York': 'Eastern Time (US & Canada)', 'America/New_York': '[UTC -4] Eastern Time (US & Canada)',
}; };
mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture); mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
......
...@@ -224,6 +224,7 @@ ci_pipelines: ...@@ -224,6 +224,7 @@ ci_pipelines:
- builds - builds
- bridges - bridges
- processables - processables
- generic_commit_statuses
- trigger_requests - trigger_requests
- variables - variables
- auto_canceled_by - auto_canceled_by
......
...@@ -645,6 +645,16 @@ RSpec.describe Member do ...@@ -645,6 +645,16 @@ RSpec.describe Member do
expect(user.authorized_projects.reload).to include(project) expect(user.authorized_projects.reload).to include(project)
end end
it 'does not accept the invite if saving a new user fails' do
invalid_user = User.new(first_name: '', last_name: '')
member.accept_invite! invalid_user
expect(member.invite_accepted_at).to be_nil
expect(member.invite_token).not_to be_nil
expect_any_instance_of(Member).not_to receive(:after_accept_invite)
end
end end
describe "#decline_invite!" do describe "#decline_invite!" do
......
...@@ -1576,6 +1576,14 @@ RSpec.describe Note do ...@@ -1576,6 +1576,14 @@ RSpec.describe Note do
expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{note.author.cache_key}") expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{note.author.cache_key}")
end end
context 'when note has no author' do
let(:note) { build(:note, author: nil) }
it 'returns cache key only' do
expect(note.post_processed_cache_key).to eq("#{note.cache_key}:")
end
end
context 'when note has redacted_note_html' do context 'when note has redacted_note_html' do
let(:redacted_note_html) { 'redacted note html' } let(:redacted_note_html) { 'redacted note html' }
......
...@@ -311,6 +311,10 @@ RSpec.describe 'getting pipeline information nested in a project' do ...@@ -311,6 +311,10 @@ RSpec.describe 'getting pipeline information nested in a project' do
end end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
# create extra statuses
create(:generic_commit_status, :pending, name: 'generic-build-a', pipeline: pipeline, stage_idx: 0, stage: 'build')
create(:ci_bridge, :failed, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
# warm up # warm up
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
...@@ -318,9 +322,11 @@ RSpec.describe 'getting pipeline information nested in a project' do ...@@ -318,9 +322,11 @@ RSpec.describe 'getting pipeline information nested in a project' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
create(:ci_build, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test') create(:generic_commit_status, :pending, name: 'generic-build-b', pipeline: pipeline, stage_idx: 0, stage: 'build')
create(:ci_build, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test') create(:ci_build, :failed, name: 'test-a', pipeline: pipeline, stage_idx: 1, stage: 'test')
create(:ci_build, name: 'deploy-a', pipeline: pipeline, stage_idx: 2, stage: 'deploy') create(:ci_build, :running, name: 'test-b', pipeline: pipeline, stage_idx: 1, stage: 'test')
create(:ci_build, :pending, name: 'deploy-b', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
create(:ci_bridge, :failed, name: 'deploy-c', pipeline: pipeline, stage_idx: 2, stage: 'deploy')
expect do expect do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
......
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