Commit fa29f13c authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents ad1824fc 2b719bdf
......@@ -41,6 +41,11 @@ export default {
type: Object,
required: true,
},
hideLineNumbers: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
viewer() {
......@@ -80,6 +85,7 @@ export default {
:is-raw-content="isRawContent"
:file-name="blob.name"
:type="activeViewer.fileType"
:hide-line-numbers="hideLineNumbers"
data-qa-selector="file_content"
/>
</template>
......
......@@ -42,9 +42,6 @@ export default {
this.switchViewer(
this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
);
if (this.hasRichViewer && !this.blobViewer) {
this.loadLegacyViewer();
}
},
error() {
this.displayError();
......@@ -69,6 +66,7 @@ export default {
data() {
return {
legacyRichViewer: null,
legacySimpleViewer: null,
isBinary: false,
isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
......@@ -115,7 +113,7 @@ export default {
return isLoggedIn();
},
isLoading() {
return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
return this.$apollo.queries.project.loading;
},
isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
......@@ -153,22 +151,41 @@ export default {
},
},
methods: {
loadLegacyViewer() {
loadLegacyViewer(type) {
if (this.legacyViewerLoaded(type)) {
return;
}
this.isLoadingLegacyViewer = true;
axios
.get(`${this.blobInfo.webPath}?format=json&viewer=rich`)
.get(`${this.blobInfo.webPath}?format=json&viewer=${type}`)
.then(({ data: { html, binary } }) => {
this.legacyRichViewer = html;
if (type === 'simple') {
this.legacySimpleViewer = html;
} else {
this.legacyRichViewer = html;
}
this.isBinary = binary;
this.isLoadingLegacyViewer = false;
})
.catch(() => this.displayError());
},
legacyViewerLoaded(type) {
return (
(type === SIMPLE_BLOB_VIEWER && this.legacySimpleViewer) ||
(type === RICH_BLOB_VIEWER && this.legacyRichViewer)
);
},
displayError() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
if (!this.blobViewer) {
this.loadLegacyViewer(this.activeViewerType);
}
},
},
};
......@@ -210,10 +227,11 @@ export default {
v-if="!blobViewer"
:rich-viewer="legacyRichViewer"
:blob="blobInfo"
:content="blobInfo.rawTextBlob"
:content="legacySimpleViewer"
:is-raw-content="true"
:active-viewer="viewer"
:loading="false"
:hide-line-numbers="true"
:loading="isLoadingLegacyViewer"
/>
<component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" />
</div>
......
......@@ -3,7 +3,9 @@ export const loadViewer = (type) => {
case 'empty':
return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue');
case 'text':
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
return gon.features.refactorTextViewer
? () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue')
: null;
case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
case 'image':
......
......@@ -27,6 +27,11 @@ export default {
required: false,
default: '',
},
hideLineNumbers: {
type: Boolean,
required: false,
default: false,
},
},
mounted() {
eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT);
......
......@@ -8,8 +8,6 @@ export default {
name: 'SimpleViewer',
components: {
GlIcon,
SourceEditor: () =>
import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
},
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
......@@ -22,9 +20,6 @@ export default {
lineNumbers() {
return this.content.split('\n').length;
},
refactorBlobViewerEnabled() {
return this.glFeatures.refactorBlobViewer;
},
},
mounted() {
const { hash } = window.location;
......@@ -52,14 +47,8 @@ export default {
</script>
<template>
<div>
<source-editor
v-if="isRawContent && refactorBlobViewerEnabled"
:value="content"
:file-name="fileName"
:editor-options="{ readOnly: true }"
/>
<div v-else class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
<div class="line-numbers">
<div class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
<div v-if="!hideLineNumbers" class="line-numbers">
<a
v-for="line in lineNumbers"
:id="`L${line}`"
......
......@@ -43,6 +43,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end
......
......@@ -34,6 +34,7 @@ class ProjectsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
end
......
......@@ -178,7 +178,13 @@ class Member < ApplicationRecord
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
after_save :log_invitation_token_cleanup
after_commit :refresh_member_authorized_projects, unless: :importing?
after_commit on: [:create, :update], unless: :importing? do
refresh_member_authorized_projects(blocking: true)
end
after_commit on: [:destroy], unless: :importing? do
refresh_member_authorized_projects(blocking: Feature.disabled?(:member_destroy_async_auth_refresh, type: :ops))
end
default_value_for :notification_level, NotificationSetting.levels[:global]
......@@ -395,8 +401,8 @@ class Member < ApplicationRecord
# transaction has been committed, resulting in the job either throwing an
# error or not doing any meaningful work.
# rubocop: disable CodeReuse/ServiceClass
def refresh_member_authorized_projects
UserProjectAccessChangedService.new(user_id).execute
def refresh_member_authorized_projects(blocking:)
UserProjectAccessChangedService.new(user_id).execute(blocking: blocking)
end
# rubocop: enable CodeReuse/ServiceClass
......
......@@ -50,8 +50,10 @@ class GroupMember < Member
{ group: group }
end
private
override :refresh_member_authorized_projects
def refresh_member_authorized_projects
def refresh_member_authorized_projects(blocking:)
# Here, `destroyed_by_association` will be present if the
# GroupMember is being destroyed due to the `dependent: :destroy`
# callback on Group. In this case, there is no need to refresh the
......@@ -63,8 +65,6 @@ class GroupMember < Member
super
end
private
def access_level_inclusion
return if access_level.in?(Gitlab::Access.all_values)
......
......@@ -90,24 +90,28 @@ class ProjectMember < Member
{ project: project }
end
private
override :refresh_member_authorized_projects
def refresh_member_authorized_projects
def refresh_member_authorized_projects(blocking:)
return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh)
return unless user
# rubocop:disable CodeReuse/ServiceClass
AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
if blocking
AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
else
AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.perform_async(project.id, user.id)
end
# Until we compare the inconsistency rates of the new, specialized service and
# the old approach, we still run AuthorizedProjectsWorker
# but with some delay and lower urgency as a safety net.
UserProjectAccessChangedService.new(user_id)
.execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY)
.execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY)
# rubocop:enable CodeReuse/ServiceClass
end
private
def send_invite
run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) }
......
......@@ -318,6 +318,7 @@ class User < ApplicationRecord
delegate :webauthn_xid, :webauthn_xid=, 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 :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
......
......@@ -3,8 +3,11 @@
class UserDetail < ApplicationRecord
extend ::Gitlab::Utils::Override
include IgnorableColumns
ignore_columns %i[bio_html cached_markdown_version], remove_with: '13.6', remove_after: '2021-10-22'
REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze
belongs_to :user
validates :pronouns, length: { maximum: 50 }
......@@ -14,6 +17,8 @@ class UserDetail < ApplicationRecord
before_save :prevent_nil_bio
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
private
def prevent_nil_bio
......
......@@ -30,6 +30,15 @@
:weight: 1
:idempotent: true
:tags: []
- :name: authorized_project_update:authorized_project_update_project_recalculate_per_user
:worker_name: AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_from_replica
:worker_name: AuthorizedProjectUpdate::UserRefreshFromReplicaWorker
:feature_category: :authentication_and_authorization
......
# frozen_string_literal: true
module AuthorizedProjectUpdate
class ProjectRecalculatePerUserWorker < ProjectRecalculateWorker
data_consistency :always
feature_category :authentication_and_authorization
urgency :high
queue_namespace :authorized_project_update
deduplicate :until_executing, including_scheduled: true
idempotent!
def perform(project_id, user_id)
project = Project.find_by_id(project_id)
user = User.find_by_id(user_id)
return unless project && user
in_lock(lock_key(project), ttl: 10.seconds) do
AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
end
end
end
end
......@@ -26,7 +26,9 @@ module AuthorizedProjectUpdate
private
def lock_key(project)
"#{self.class.name.underscore}/projects/#{project.id}"
# The self.class.name.underscore value is hardcoded here as the prefix, so that the same
# lock_key for this superclass will be used by the ProjectRecalculatePerUserWorker subclass.
"authorized_project_update/project_recalculate_worker/projects/#{project.id}"
end
end
end
---
name: refactor_text_viewer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70909
rollout_issue_url:
milestone: '14.4'
type: development
group: 'group::source code'
default_enabled: false
---
name: member_destroy_async_auth_refresh
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66424
rollout_issue_url:
milestone: '14.4'
type: ops
group: group::access
default_enabled: false
# frozen_string_literal: true
class AddRegistrationObjectiveToUserDetail < Gitlab::Database::Migration[1.0]
def change
add_column :user_details, :registration_objective, :smallint
end
end
9204c844b22ad0d3a938ed908377c8baacdda038725a5cf105e4b11841c1ae21
\ No newline at end of file
......@@ -19764,6 +19764,7 @@ CREATE TABLE user_details (
provisioned_by_group_id bigint,
pronouns text,
pronunciation text,
registration_objective smallint,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100)),
CONSTRAINT check_eeeaf8d4f0 CHECK ((char_length(pronouns) <= 50)),
......@@ -4,7 +4,7 @@ import Tracking from '~/tracking';
const select = document.querySelector('.js-jobs-to-be-done-dropdown');
if (select) {
Tracking.enableFormTracking(
{ fields: { allow: ['jobs_to_be_done', 'jobs_to_be_done_other'] } },
{ fields: { allow: ['jobs_to_be_done_other'] } },
getExperimentContexts('jobs_to_be_done'),
);
......
......@@ -44,7 +44,7 @@ module EE
override :update_params
def update_params
clean_params = super.merge(params.require(:user).permit(:email_opted_in))
clean_params = super.merge(params.require(:user).permit(:email_opted_in, :registration_objective))
return clean_params unless ::Gitlab.dev_env_or_com?
......
......@@ -10,8 +10,10 @@ module EE
super.merge(api_path: suggestion_path)
end
def shuffled_jobs_to_be_done_options
jobs_to_be_done_options.shuffle.append([_('A different reason'), 'other'])
def shuffled_registration_objective_options
options = registration_objective_options
other = options.extract!(:other).to_a.flatten
options.to_a.shuffle.append(other).map { |option| option.reverse }
end
private
......@@ -23,15 +25,10 @@ module EE
end
end
def jobs_to_be_done_options
[
_('I want to learn the basics of Git'),
_('I want to move my repository to GitLab from somewhere else'),
_('I want to store my code'),
_('I want to explore GitLab to see if it’s worth switching to'),
_('I want to use GitLab CI with my existing repository'),
_('I’m joining my team who’s already on GitLab')
]
def registration_objective_options
localized_jobs_to_be_done_choices.merge(
joining_team: _('I’m joining my team who’s already on GitLab')
)
end
end
end
......@@ -2,10 +2,8 @@
- e.try do
.row
.form-group.col-sm-12
= label_tag :jobs_to_be_done, _("I'm signing up for GitLab because:")
= select_tag :jobs_to_be_done,
options_for_select(shuffled_jobs_to_be_done_options),
include_blank: _('Please select...'), class: 'form-control js-jobs-to-be-done-dropdown'
= label_tag :user_registration_objective, _("I'm signing up for GitLab because:")
= f.select :registration_objective, shuffled_registration_objective_options, { include_blank: _('Please select...') }, class: 'form-control js-jobs-to-be-done-dropdown'
.row
.form-group.col-sm-12.js-jobs-to-be-done-other-group.hidden
= label_tag :jobs_to_be_done_other, _('Why are you signing up? (Optional)')
......
......@@ -135,7 +135,8 @@ RSpec.describe Registrations::WelcomeController do
user: {
role: 'software_developer',
setup_for_company: setup_for_company,
email_opted_in: email_opted_in
email_opted_in: email_opted_in,
registration_objective: 'code_storage'
}
}
end
......@@ -179,6 +180,14 @@ RSpec.describe Registrations::WelcomeController do
allow(::Gitlab).to receive(:com?).and_return(true)
end
context 'when registration_objective field is provided' do
it 'sets the registration_objective' do
subject
expect(controller.current_user.registration_objective).to eq('code_storage')
end
end
context 'when setup for company is false' do
context 'when the user opted in' do
let(:email_opted_in) { '1' }
......
......@@ -59,7 +59,7 @@ RSpec.describe 'Welcome screen', :js do
it 'allows specifying other for the jobs_to_be_done experiment', :experiment do
expect(page).not_to have_content('Why are you signing up? (Optional)')
select 'A different reason', from: 'jobs_to_be_done'
select 'A different reason', from: 'user_registration_objective'
expect(page).to have_content('Why are you signing up? (Optional)')
......
......@@ -9,25 +9,17 @@ RSpec.describe EE::RegistrationsHelper do
end
end
describe '#shuffled_jobs_to_be_done_options' do
subject { helper.shuffled_jobs_to_be_done_options }
describe '#shuffled_registration_objective_options' do
subject(:shuffled_options) { helper.shuffled_registration_objective_options }
let(:array_double) { double(:array) }
it 'has values that match all UserDetail registration objective keys' do
shuffled_option_values = shuffled_options.map { |item| item.last }
it 'uses shuffle' do
allow(helper).to receive(:jobs_to_be_done_options).and_return(array_double)
expect(array_double).to receive(:shuffle).and_return([])
subject
end
it 'has a number of options' do
expect(subject.count).to eq(7)
expect(shuffled_option_values).to contain_exactly(*UserDetail.registration_objectives.keys)
end
it '"other" is always the last option' do
expect(subject.last).to eq(['A different reason', 'other'])
expect(shuffled_options.last).to eq(['A different reason', 'other'])
end
end
end
......@@ -282,7 +282,7 @@ RSpec.describe EE::Gitlab::Auth::Ldap::Sync::Group do
.to eq(::Gitlab::Access::OWNER)
end
it 'updates projects authorizations' do
it 'updates projects authorizations', :sidekiq_inline do
project = create(:project, namespace: group)
group.add_user(user, Gitlab::Access::MAINTAINER)
......
......@@ -1336,7 +1336,7 @@ RSpec.describe ApprovalState do
expect(subject.can_approve?(nil)).to be_falsey
end
context 'when an approver does not have access to the merge request' do
context 'when an approver does not have access to the merge request', :sidekiq_inline do
before do
project.members.find_by(user_id: developer.id).destroy!
end
......
......@@ -324,7 +324,7 @@ RSpec.describe TodoService do
let(:project) { create(:project, :private, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, author: author) }
context 'an approver has lost access to the project' do
context 'an approver has lost access to the project', :sidekiq_inline do
before do
create(:approver, user: non_member, target: project)
project.members.find_by(user_id: non_member.id).destroy
......
......@@ -66,7 +66,7 @@ RSpec.describe 'registrations/welcome/show' do
let_it_be(:stubbed_experiments) { { jobs_to_be_done: :candidate } }
it 'renders a select and text field for additional information' do
is_expected.to have_selector('select[name="jobs_to_be_done"]')
is_expected.to have_selector('select[name="user[registration_objective]"]')
is_expected.to have_selector('input[name="jobs_to_be_done_other"]', visible: false)
end
end
......
......@@ -239,7 +239,7 @@ RSpec.describe Projects::BranchesController do
end
end
context 'without issue feature access' do
context 'without issue feature access', :sidekiq_inline do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
......
......@@ -409,7 +409,7 @@ RSpec.describe Projects::CompareController do
end
end
context 'when the user does not have access to the project' do
context 'when the user does not have access to the project', :sidekiq_inline do
before do
project.team.truncate
project.update!(visibility: 'private')
......
......@@ -159,8 +159,13 @@ describe('Blob content viewer component', () => {
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
beforeEach(() => {
gon.features = { refactorTextViewer: true };
});
afterEach(() => {
wrapper.destroy();
mockAxios.reset();
});
it('renders a GlLoadingIcon component', () => {
......@@ -183,7 +188,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'text',
......@@ -192,6 +196,16 @@ describe('Blob content viewer component', () => {
renderError: null,
});
});
describe('legacy viewers', () => {
it('loads a legacy viewer when a viewer component is not available', async () => {
createComponentWithApollo({ blobs: { ...simpleMockData, fileType: 'unknown' } });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=simple');
});
});
});
describe('rich viewer', () => {
......@@ -210,7 +224,6 @@ describe('Blob content viewer component', () => {
it('renders a BlobContent component', () => {
expect(findBlobContent().props('loading')).toEqual(false);
expect(findBlobContent().props('content')).toEqual('raw content');
expect(findBlobContent().props('isRawContent')).toBe(true);
expect(findBlobContent().props('activeViewer')).toEqual({
fileType: 'markup',
......@@ -241,18 +254,12 @@ describe('Blob content viewer component', () => {
});
describe('legacy viewers', () => {
it('does not load a legacy viewer when a rich viewer is not available', async () => {
createComponentWithApollo({ blobs: simpleMockData });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(0);
});
it('loads a legacy viewer when a rich viewer is available', async () => {
createComponentWithApollo({ blobs: richMockData });
it('loads a legacy viewer when a viewer component is not available', async () => {
createComponentWithApollo({ blobs: { ...richMockData, fileType: 'unknown' } });
await waitForPromises();
expect(mockAxios.history.get).toHaveLength(1);
expect(mockAxios.history.get[0].url).toEqual('some_file.js?format=json&viewer=rich');
});
});
......
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
describe('Blob Simple Viewer component', () => {
let wrapper;
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
const blobHash = 'foo-bar';
function createComponent(
content = contentMock,
isRawContent = false,
isRefactorFlagEnabled = false,
) {
function createComponent(content = contentMock, isRawContent = false) {
wrapper = shallowMount(SimpleViewer, {
provide: {
blobHash,
glFeatures: {
refactorBlobViewer: isRefactorFlagEnabled,
},
},
propsData: {
content,
......@@ -94,32 +85,4 @@ describe('Blob Simple Viewer component', () => {
});
});
});
describe('Vue refactoring to use Source Editor', () => {
const findSourceEditor = () => wrapper.find(SourceEditor);
it.each`
doesRender | condition | isRawContent | isRefactorFlagEnabled
${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true}
${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false}
${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
`(
'$doesRender render Source Editor component in readonly mode when $condition',
async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
createComponent('raw content', isRawContent, isRefactorFlagEnabled);
await waitForPromises();
if (isRawContent && isRefactorFlagEnabled) {
expect(findSourceEditor().exists()).toBe(true);
expect(findSourceEditor().props('value')).toBe('raw content');
expect(findSourceEditor().props('fileName')).toBe('test.js');
expect(findSourceEditor().props('editorOptions')).toEqual({ readOnly: true });
} else {
expect(findSourceEditor().exists()).toBe(false);
}
},
);
});
});
......@@ -187,7 +187,7 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(analyzer['enabled']).to eq(true)
end
context "with guest user" do
context 'with guest user' do
before do
project.add_guest(user)
end
......@@ -195,7 +195,7 @@ RSpec.describe GitlabSchema.types['Project'] do
context 'when project is private' do
let(:project) { create(:project, :private, :repository) }
it "returns no configuration" do
it 'returns no configuration' do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
end
......@@ -215,7 +215,7 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
context "with non-member user" do
context 'with non-member user', :sidekiq_inline do
before do
project.team.truncate
end
......@@ -223,7 +223,7 @@ RSpec.describe GitlabSchema.types['Project'] do
context 'when project is private' do
let(:project) { create(:project, :private, :repository) }
it "returns no configuration" do
it 'returns no configuration' do
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
end
......@@ -241,7 +241,7 @@ RSpec.describe GitlabSchema.types['Project'] do
end
context 'when repository is accessible only by team members' do
it "returns no configuration" do
it 'returns no configuration' do
project.project_feature.update!(
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
......
......@@ -98,7 +98,7 @@ RSpec.describe Gitlab::Middleware::Go do
end
end
context 'without access to the project' do
context 'without access to the project', :sidekiq_inline do
before do
project.team.find_member(current_user).destroy
end
......
......@@ -95,7 +95,7 @@ RSpec.describe Gitlab::SlashCommands::IssueMove, service: true do
end
end
context 'when the user cannot see the target project' do
context 'when the user cannot see the target project', :sidekiq_inline do
it 'returns not found' do
message = "issue move #{issue.iid} #{other_project.full_path}"
other_project.team.truncate
......
......@@ -7,11 +7,11 @@ RSpec.describe Member do
using RSpec::Parameterized::TableSyntax
describe "Associations" do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
end
describe "Validation" do
describe 'Validation' do
subject { described_class.new(access_level: Member::GUEST) }
it { is_expected.to validate_presence_of(:user) }
......@@ -28,7 +28,7 @@ RSpec.describe Member do
subject { build(:project_member) }
end
context "when an invite email is provided" do
context 'when an invite email is provided' do
let_it_be(:project) { create(:project) }
let(:member) { build(:project_member, source: project, invite_email: "user@example.com", user: nil) }
......@@ -37,29 +37,29 @@ RSpec.describe Member do
expect(member).to be_valid
end
it "requires a valid invite email" do
it 'requires a valid invite email' do
member.invite_email = "nope"
expect(member).not_to be_valid
end
it "requires a unique invite email scoped to this source" do
it 'requires a unique invite email scoped to this source' do
create(:project_member, source: member.source, invite_email: member.invite_email)
expect(member).not_to be_valid
end
end
context "when an invite email is not provided" do
context 'when an invite email is not provided' do
let(:member) { build(:project_member) }
it "requires a user" do
it 'requires a user' do
member.user = nil
expect(member).not_to be_valid
end
it "is valid otherwise" do
it 'is valid otherwise' do
expect(member).to be_valid
end
end
......@@ -107,13 +107,13 @@ RSpec.describe Member do
end
end
context "when a child member inherits its access level" do
context 'when a child member inherits its access level' do
let(:user) { create(:user) }
let(:member) { create(:group_member, :developer, user: user) }
let(:child_group) { create(:group, parent: member.group) }
let(:child_member) { build(:group_member, group: child_group, user: user) }
it "requires a higher level" do
it 'requires a higher level' do
child_member.access_level = GroupMember::REPORTER
child_member.validate
......@@ -123,7 +123,7 @@ RSpec.describe Member do
# Membership in a subgroup confers certain access rights, such as being
# able to merge or push code to protected branches.
it "is valid with an equal level" do
it 'is valid with an equal level' do
child_member.access_level = GroupMember::DEVELOPER
child_member.validate
......@@ -131,7 +131,7 @@ RSpec.describe Member do
expect(child_member).to be_valid
end
it "is valid with a higher level" do
it 'is valid with a higher level' do
child_member.access_level = GroupMember::MAINTAINER
child_member.validate
......@@ -538,7 +538,7 @@ RSpec.describe Member do
end
end
describe "Delegate methods" do
describe 'Delegate methods' do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
end
......@@ -608,29 +608,29 @@ RSpec.describe Member do
end
end
describe "#accept_invite!" do
describe '#accept_invite!' do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
it "resets the invite token" do
it 'resets the invite token' do
member.accept_invite!(user)
expect(member.invite_token).to be_nil
end
it "sets the invite accepted timestamp" do
it 'sets the invite accepted timestamp' do
member.accept_invite!(user)
expect(member.invite_accepted_at).not_to be_nil
end
it "sets the user" do
it 'sets the user' do
member.accept_invite!(user)
expect(member.user).to eq(user)
end
it "calls #after_accept_invite" do
it 'calls #after_accept_invite' do
expect(member).to receive(:after_accept_invite)
member.accept_invite!(user)
......@@ -657,26 +657,26 @@ RSpec.describe Member do
end
end
describe "#decline_invite!" do
describe '#decline_invite!' do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
it "destroys the member" do
it 'destroys the member' do
member.decline_invite!
expect(member).to be_destroyed
end
it "calls #after_decline_invite" do
it 'calls #after_decline_invite' do
expect(member).to receive(:after_decline_invite)
member.decline_invite!
end
end
describe "#generate_invite_token" do
describe '#generate_invite_token' do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
it "sets the invite token" do
it 'sets the invite token' do
expect { member.generate_invite_token }.to change { member.invite_token }
end
end
......@@ -684,12 +684,12 @@ RSpec.describe Member do
describe 'generate invite token on create' do
let!(:member) { build(:project_member, invite_email: "user@example.com") }
it "sets the invite token" do
it 'sets the invite token' do
expect { member.save! }.to change { member.invite_token }.to(kind_of(String))
end
context 'when invite was already accepted' do
it "does not set invite token" do
it 'does not set invite token' do
member.invite_accepted_at = 1.day.ago
expect { member.save! }.not_to change { member.invite_token }.from(nil)
......@@ -744,7 +744,7 @@ RSpec.describe Member do
end
end
describe "#invite_to_unknown_user?" do
describe '#invite_to_unknown_user?' do
subject { member.invite_to_unknown_user? }
let(:member) { create(:project_member, invite_email: "user@example.com", invite_token: '1234', user: user) }
......@@ -762,7 +762,7 @@ RSpec.describe Member do
end
end
describe "destroying a record", :delete do
describe 'destroying a record', :delete, :sidekiq_inline do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
......
......@@ -244,12 +244,32 @@ RSpec.describe ProjectMember do
project.add_user(user, Gitlab::Access::GUEST)
end
it 'changes access level' do
expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
context 'when :member_destroy_async_auth_refresh feature flag is enabled' do
it 'changes access level', :sidekiq_inline do
expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
end
it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker to recalculate authorizations' do
expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).to receive(:perform_async).with(project.id, user.id)
action
end
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService to recalculate authorizations'
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
context 'when :member_destroy_async_auth_refresh feature flag is disabled' do
before do
stub_feature_flags(member_destroy_async_auth_refresh: false)
end
it 'changes access level' do
expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
end
it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService to recalculate authorizations'
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
end
context 'when the feature flag `specialized_service_for_project_member_auth_refresh` is disabled' do
......@@ -298,7 +318,7 @@ RSpec.describe ProjectMember do
project.add_user(user, Gitlab::Access::GUEST)
end
it 'changes access level' do
it 'changes access level', :sidekiq_inline do
expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
end
......
......@@ -11,6 +11,8 @@ RSpec.describe NamespaceSetting, type: :model do
it { is_expected.to belong_to(:namespace) }
end
it { is_expected.to define_enum_for(:jobs_to_be_done).with_values([:basics, :move_repository, :code_storage, :exploring, :ci, :other]).with_suffix }
describe "validations" do
describe "#default_branch_name_content" do
let_it_be(:group) { create(:group) }
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe UserDetail do
it { is_expected.to belong_to(:user) }
it { is_expected.to define_enum_for(:registration_objective).with_values([:basics, :move_repository, :code_storage, :exploring, :ci, :other, :joining_team]).with_suffix }
describe 'validations' do
describe '#job_title' do
......
This diff is collapsed.
......@@ -38,7 +38,7 @@ RSpec.describe API::PackageFiles do
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 404 for a user without access to the project' do
it 'returns 404 for a user without access to the project', :sidekiq_inline do
project.team.truncate
get api(url, user)
......
......@@ -275,7 +275,7 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
expect(subject[:merge_pipeline]).to be_nil
end
context 'when is merged' do
context 'when is merged', :sidekiq_inline do
let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) }
......
......@@ -17,7 +17,7 @@ RSpec.describe MergeRequests::AssignIssuesService do
expect(service.assignable_issues.map(&:id)).to include(issue.id)
end
it 'ignores issues the user cannot update assignee on' do
it 'ignores issues the user cannot update assignee on', :sidekiq_inline do
project.team.truncate
expect(service.assignable_issues).to be_empty
......
......@@ -440,7 +440,7 @@ RSpec.describe MergeRequests::BuildService do
expect(merge_request.title).to eq('Closes #1234 Second commit')
end
it 'adds the remaining lines of the first multi-line commit message as the description' do
it 'adds the remaining lines of the first multi-line commit message as the description', :sidekiq_inline do
expect(merge_request.description).to eq('Create the app')
end
end
......
......@@ -701,7 +701,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do
let(:push_options) { { create: true } }
let(:changes) { new_branch_changes }
it 'records an error' do
it 'records an error', :sidekiq_inline do
Members::DestroyService.new(user1).execute(ProjectMember.find_by!(user_id: user1.id))
service.execute
......
......@@ -47,7 +47,7 @@ RSpec.describe Notes::QuickActionsService do
let(:note_text) { "/relate #{other_issue.to_reference}" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
context 'user cannot relate issues' do
context 'user cannot relate issues', :sidekiq_inline do
before do
project.team.find_member(maintainer.id).destroy!
project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
......
......@@ -3155,7 +3155,7 @@ RSpec.describe NotificationService, :mailer do
notification.pipeline_finished(pipeline)
end
it 'does not send emails' do
it 'does not send emails', :sidekiq_inline do
should_not_email_anyone
end
end
......
......@@ -26,7 +26,7 @@ RSpec.describe Projects::MoveAccessService do
describe '#execute' do
shared_examples 'move the accesses' do
it do
it 'moves the accesses', :sidekiq_inline do
expect(project_with_access.project_members.count).to eq 4
expect(project_with_access.project_group_links.count).to eq 3
expect(project_with_access.authorized_users.count).to eq 4
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker do
include ExclusiveLeaseHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
subject(:worker) { described_class.new }
include_examples 'an idempotent worker' do
let(:job_args) { [project.id, user.id] }
it 'does not change authorizations when run twice' do
project.add_developer(user)
user.project_authorizations.delete_all
expect { worker.perform(project.id, user.id) }.to change { project.project_authorizations.reload.size }.by(1)
expect { worker.perform(project.id, user.id) }.not_to change { project.project_authorizations.reload.size }
end
end
describe '#perform' do
it 'does not fail if the project does not exist' do
expect do
worker.perform(non_existing_record_id, user.id)
end.not_to raise_error
end
it 'does not fail if the user does not exist' do
expect do
worker.perform(project.id, non_existing_record_id)
end.not_to raise_error
end
it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserService' do
expect_next_instance_of(AuthorizedProjectUpdate::ProjectRecalculatePerUserService, project, user) do |service|
expect(service).to receive(:execute)
end
worker.perform(project.id, user.id)
end
context 'exclusive lease' do
let(:lock_key) { "#{described_class.superclass.name.underscore}/projects/#{project.id}" }
let(:timeout) { 10.seconds }
context 'when exclusive lease has not been taken' do
it 'obtains a new exclusive lease' do
expect_to_obtain_exclusive_lease(lock_key, timeout: timeout)
worker.perform(project.id, user.id)
end
end
context 'when exclusive lease has already been taken' do
before do
stub_exclusive_lease_taken(lock_key, timeout: timeout)
end
it 'raises an error' do
expect { worker.perform(project.id, user.id) }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
end
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