Commit 8b757f2f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents ff42f4f7 af144667
87acab96b9eb16381a49f2c08a2eaa9664a2fa75
3f5e218def93024f3aafe590c22cd1b29f744105
<script>
import { GlAvatarLink, GlAvatarLabeled, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
import { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants';
......@@ -7,7 +13,11 @@ export default {
name: 'UserAvatar',
avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'),
components: { GlAvatarLink, GlAvatarLabeled },
components: {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
},
directives: {
SafeHtml,
},
......@@ -16,11 +26,18 @@ export default {
type: Object,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
},
computed: {
user() {
return this.member.user;
},
badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
},
},
};
</script>
......@@ -41,7 +58,15 @@ export default {
:size="$options.avatarSize"
:entity-name="user.name"
:entity-id="user.id"
/>
>
<template #meta>
<div v-for="badge in badges" :key="badge.text" class="gl-p-1">
<gl-badge size="sm" :variant="badge.variant">
{{ badge.text }}
</gl-badge>
</div>
</template>
</gl-avatar-labeled>
</gl-avatar-link>
<gl-avatar-labeled
......
......@@ -12,6 +12,10 @@ export default {
type: String,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
member: {
type: Object,
required: true,
......@@ -27,5 +31,5 @@ export default {
</script>
<template>
<component :is="avatarComponent" :member="member" />
<component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" />
</template>
......@@ -44,8 +44,12 @@ export default {
show-empty
>
<template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType }" :member="member">
<member-avatar :member-type="memberType" :member="member" />
<members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell>
</template>
......
......@@ -11,7 +11,7 @@ export default {
},
},
computed: {
...mapState(['sourceId']),
...mapState(['sourceId', 'currentUserId']),
isGroup() {
return Boolean(this.member.sharedWithGroup);
},
......@@ -35,11 +35,15 @@ export default {
isDirectMember() {
return this.member.source?.id === this.sourceId;
},
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
},
},
render() {
return this.$scopedSlots.default({
memberType: this.memberType,
isDirectMember: this.isDirectMember,
isCurrentUser: this.isCurrentUser,
});
},
};
......
import { __ } from '~/locale';
export const generateBadges = (member, isCurrentUser) => [
{
show: isCurrentUser,
text: __("It's you"),
variant: 'success',
},
{
show: member.user?.blocked,
text: __('Blocked'),
variant: 'danger',
},
{
show: member.user?.twoFactorEnabled,
text: __('2FA'),
variant: 'info',
},
];
......@@ -14,6 +14,8 @@ class ApplicationSetting
end
def accepted_by_user?(user)
return true if user.project_bot?
user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists?
end
......
......@@ -1674,6 +1674,8 @@ class User < ApplicationRecord
end
def terms_accepted?
return true if project_bot?
accepted_term_id.present?
end
......
---
title: Auto-accept TOS if project bot
merge_request: 43067
author:
type: fixed
......@@ -83,7 +83,7 @@ There are some high level differences between the products worth mentioning:
- You can control which jobs run in which cases, depending on how they are triggered,
with the [`rules` syntax](../yaml/README.md#rules).
- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different than with Jenkins.
- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different from Jenkins.
- You can reuse pipeline configurations using the [`include` keyword](../yaml/README.md#include)
and [templates](#templates). Your templates can be kept in a central repository (with different
permissions), and then any project can use them. This central project could also
......
......@@ -33,11 +33,8 @@ We have complete examples of configuring pipelines:
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Learn how [Verizon reduced rebuilds](https://about.gitlab.com/blog/2019/02/14/verizon-customer-story/)
> from 30 days to under 8 hours with GitLab.
NOTE: **Note:**
If you have a [mirrored repository that GitLab pulls from](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository),
you may need to enable pipeline triggering. Go to your project's
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
you may need to enable pipeline triggering. Go to your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
## Introduction
......@@ -961,7 +958,7 @@ GitLab performs a reverse deep merge based on the keys. GitLab:
- Merges the `rspec` contents into `.tests` recursively.
- Doesn't merge the values of the keys.
The result is this `rspec` job:
The result is this `rspec` job, where `script: rake test` is overwritten by `script: rake rspec`:
```yaml
rspec:
......@@ -974,9 +971,6 @@ rspec:
- $RSPEC
```
NOTE: **Note:**
Note that `script: rake test` has been overwritten by `script: rake rspec`.
If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script).
`.tests` in this example is a [hidden job](#hide-jobs), but it's
......
import { __ } from '~/locale';
import { generateBadges as CEGenerateBadges } from '~/vue_shared/components/members/utils';
export const generateBadges = (member, isCurrentUser) => [
...CEGenerateBadges(member, isCurrentUser),
{
show: member.usingLicense,
text: __('Is using seat'),
variant: 'neutral',
},
{
show: member.groupSso,
text: __('SAML'),
variant: 'info',
},
{
show: member.groupManagedAccount,
text: __('Managed Account'),
variant: 'info',
},
];
import { member as memberMock } from 'jest/vue_shared/components/members/mock_data';
import { generateBadges } from 'ee/vue_shared/components/members/utils';
describe('Members Utils', () => {
describe('generateBadges', () => {
it('has correct properties for each badge', () => {
const badges = generateBadges(memberMock, true);
badges.forEach(badge => {
expect(badge).toEqual(
expect.objectContaining({
show: expect.any(Boolean),
text: expect.any(String),
variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/),
}),
);
});
});
it.each`
member | expected
${{ ...memberMock, usingLicense: true }} | ${{ show: true, text: 'Is using seat', variant: 'neutral' }}
${{ ...memberMock, groupSso: true }} | ${{ show: true, text: 'SAML', variant: 'info' }}
${{ ...memberMock, groupManagedAccount: true }} | ${{ show: true, text: 'Managed Account', variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected));
});
});
});
......@@ -22181,6 +22181,9 @@ msgstr ""
msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr ""
msgid "SAML"
msgstr ""
msgid "SAML SSO"
msgstr ""
......
......@@ -5,6 +5,12 @@ module QA
describe 'LDAP Group sync' do
include Support::Api
let(:group) do
Resource::Group.fabricate_via_api! do |resource|
resource.path = "#{group_name}-#{SecureRandom.hex(4)}"
end
end
before(:all) do
# Create the sandbox group as the LDAP user. Without this the admin user
# would own the sandbox group and then in subsequent tests the LDAP user
......@@ -62,10 +68,16 @@ module QA
let(:owner_user) { 'enguser1' }
let(:sync_users) { ['ENG User 2', 'ENG User 3'] }
let(:group_name) { 'Synched-engineering-group' }
before do
@created_users = create_users_via_api(ldap_users)
group = create_group_and_add_user_via_api(owner_user, 'Synched-engineering-group', Resource::Members::AccessLevel::OWNER)
signin_and_visit_group_as_user(owner_user, group)
created_users = create_users_via_api(ldap_users)
group.add_member(created_users[owner_user], Resource::Members::AccessLevel::OWNER)
signin_as_user(owner_user)
group.visit!
Page::Group::Menu.perform(&:go_to_ldap_sync_settings)
......@@ -113,12 +125,16 @@ module QA
let(:owner_user) { 'hruser1' }
let(:sync_users) { ['HR User 2', 'HR User 3'] }
let(:group_name) { 'Synched-human-resources-group' }
before do
@created_users = create_users_via_api(ldap_users)
created_users = create_users_via_api(ldap_users)
group.add_member(created_users[owner_user], Resource::Members::AccessLevel::OWNER)
group = create_group_and_add_user_via_api(owner_user, 'Synched-human-resources-group', Resource::Members::AccessLevel::OWNER)
signin_as_user(owner_user)
signin_and_visit_group_as_user(owner_user, group)
group.visit!
Page::Group::Menu.perform(&:go_to_ldap_sync_settings)
......@@ -160,23 +176,23 @@ module QA
group
end
def signin_and_visit_group_as_user(user_name, group)
def signin_as_user(user_name)
user = Struct.new(:ldap_username, :ldap_password).new(user_name, 'password')
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform do |login_page|
login_page.sign_in_using_ldap_credentials(user: user)
end
group.visit!
end
def verify_users_synced(expected_users)
EE::Page::Group::Members.perform do |members|
members.click_sync_now
users_synchronised = members.retry_until(reload: true) do
expected_users.map { |user| members.has_content?(user) }.all?
end
expect(users_synchronised).to be_truthy
end
end
......
......@@ -26,6 +26,21 @@ RSpec.describe 'Users > Terms' do
expect(page).not_to have_content('Continue')
end
context 'when user is a project bot' do
let(:project_bot) { create(:user, :project_bot) }
before do
enforce_terms
end
it 'auto accepts the terms' do
visit terms_path
expect(page).not_to have_content('Accept terms')
expect(project_bot.terms_accepted?).to be(true)
end
end
context 'when signed in' do
let(:user) { create(:user) }
......
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui';
import { member, orphanedMember } from '../mock_data';
import { within } from '@testing-library/dom';
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
describe('MemberList', () => {
let wrapper;
const { user } = member;
const { user } = memberMock;
const createComponent = (propsData = {}) => {
wrapper = mount(UserAvatar, {
propsData: {
member,
member: memberMock,
isCurrentUser: false,
...propsData,
},
});
};
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
createWrapper(within(wrapper.element).findByText(text, options));
afterEach(() => {
wrapper.destroy();
......@@ -63,4 +64,25 @@ describe('MemberList', () => {
expect(getByText('Orphaned member').exists()).toBe(true);
});
});
describe('badges', () => {
it.each`
member | badgeText
${{ ...memberMock, usingLicense: true }} | ${'Is using seat'}
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'}
${{ ...memberMock, groupSso: true }} | ${'SAML'}
${{ ...memberMock, groupManagedAccount: true }} | ${'Managed Account'}
`('renders the "$badgeText" badge', ({ member, badgeText }) => {
createComponent({ member });
expect(wrapper.find(GlBadge).text()).toBe(badgeText);
});
it('renders the "It\'s you" badge when member is current user', () => {
createComponent({ isCurrentUser: true });
expect(getByText("It's you").exists()).toBe(true);
});
});
});
......@@ -11,7 +11,10 @@ describe('MemberList', () => {
const createComponent = propsData => {
wrapper = shallowMount(MemberAvatar, {
propsData,
propsData: {
isCurrentUser: false,
...propsData,
},
});
};
......
......@@ -15,6 +15,10 @@ describe('MemberList', () => {
type: Boolean,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
},
render(createElement) {
return createElement('div', this.memberType);
......@@ -29,6 +33,7 @@ describe('MemberList', () => {
return new Vuex.Store({
state: {
sourceId: 1,
currentUserId: 1,
...state,
},
});
......@@ -42,8 +47,13 @@ describe('MemberList', () => {
propsData,
store: createStore(state),
scopedSlots: {
default:
'<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />',
default: `
<wrapped-component
:member-type="props.memberType"
:is-direct-member="props.isDirectMember"
:is-current-user="props.isCurrentUser"
/>
`,
},
});
};
......@@ -93,4 +103,28 @@ describe('MemberList', () => {
expect(findWrappedComponent().props('isDirectMember')).toBe(false);
});
});
describe('isCurrentUser', () => {
it('returns `true` when `member.user` has the same ID as `currentUserId`', () => {
createComponent({
member: {
...memberMock,
user: {
...memberMock.user,
id: 1,
},
},
});
expect(findWrappedComponent().props('isCurrentUser')).toBe(true);
});
it('returns `false` when `member.user` does not have the same ID as `currentUserId`', () => {
createComponent({
member: memberMock,
});
expect(findWrappedComponent().props('isCurrentUser')).toBe(false);
});
});
});
import { generateBadges } from '~/vue_shared/components/members/utils';
import { member as memberMock } from './mock_data';
describe('Members Utils', () => {
describe('generateBadges', () => {
it('has correct properties for each badge', () => {
const badges = generateBadges(memberMock, true);
badges.forEach(badge => {
expect(badge).toEqual(
expect.objectContaining({
show: expect.any(Boolean),
text: expect.any(String),
variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/),
}),
);
});
});
it.each`
member | expected
${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }}
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${{ show: true, text: '2FA', variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected));
});
});
});
......@@ -17,6 +17,7 @@ RSpec.describe ApplicationSetting::Term do
describe '#accepted_by_user?' do
let(:user) { create(:user) }
let(:project_bot) { create(:user, :project_bot) }
let(:term) { create(:term) }
it 'is true when the user accepted the terms' do
......@@ -25,6 +26,10 @@ RSpec.describe ApplicationSetting::Term do
expect(term.accepted_by_user?(user)).to be(true)
end
it 'is true when user is a bot' do
expect(term.accepted_by_user?(project_bot)).to be(true)
end
it 'is false when the user declined the terms' do
decline_terms(term, user)
......
......@@ -4330,28 +4330,32 @@ RSpec.describe User do
describe '#required_terms_not_accepted?' do
let(:user) { build(:user) }
let(:project_bot) { create(:user, :project_bot) }
subject { user.required_terms_not_accepted? }
context "when terms are not enforced" do
it { is_expected.to be_falsy }
it { is_expected.to be_falsey }
end
context "when terms are enforced and accepted by the user" do
context "when terms are enforced" do
before do
enforce_terms
accept_terms(user)
end
it { is_expected.to be_falsy }
end
it "is not accepted by the user" do
expect(subject).to be_truthy
end
context "when terms are enforced but the user has not accepted" do
before do
enforce_terms
it "is accepted by the user" do
accept_terms(user)
expect(subject).to be_falsey
end
it { is_expected.to be_truthy }
it "auto accepts the term for project bots" do
expect(project_bot.required_terms_not_accepted?).to be_falsey
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