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

Automatic merge of gitlab-org/gitlab master

parents ff42f4f7 af144667
87acab96b9eb16381a49f2c08a2eaa9664a2fa75 3f5e218def93024f3aafe590c22cd1b29f744105
<script> <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 { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants'; import { AVATAR_SIZE } from '../constants';
...@@ -7,7 +13,11 @@ export default { ...@@ -7,7 +13,11 @@ export default {
name: 'UserAvatar', name: 'UserAvatar',
avatarSize: AVATAR_SIZE, avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'), orphanedUserLabel: __('Orphaned member'),
components: { GlAvatarLink, GlAvatarLabeled }, components: {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
},
directives: { directives: {
SafeHtml, SafeHtml,
}, },
...@@ -16,11 +26,18 @@ export default { ...@@ -16,11 +26,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
user() { user() {
return this.member.user; return this.member.user;
}, },
badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
},
}, },
}; };
</script> </script>
...@@ -41,7 +58,15 @@ export default { ...@@ -41,7 +58,15 @@ export default {
:size="$options.avatarSize" :size="$options.avatarSize"
:entity-name="user.name" :entity-name="user.name"
:entity-id="user.id" :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-link>
<gl-avatar-labeled <gl-avatar-labeled
......
...@@ -12,6 +12,10 @@ export default { ...@@ -12,6 +12,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
member: { member: {
type: Object, type: Object,
required: true, required: true,
...@@ -27,5 +31,5 @@ export default { ...@@ -27,5 +31,5 @@ export default {
</script> </script>
<template> <template>
<component :is="avatarComponent" :member="member" /> <component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" />
</template> </template>
...@@ -44,8 +44,12 @@ export default { ...@@ -44,8 +44,12 @@ export default {
show-empty show-empty
> >
<template #cell(account)="{ item: member }"> <template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType }" :member="member"> <members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar :member-type="memberType" :member="member" /> <member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell> </members-table-cell>
</template> </template>
......
...@@ -11,7 +11,7 @@ export default { ...@@ -11,7 +11,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['sourceId']), ...mapState(['sourceId', 'currentUserId']),
isGroup() { isGroup() {
return Boolean(this.member.sharedWithGroup); return Boolean(this.member.sharedWithGroup);
}, },
...@@ -35,11 +35,15 @@ export default { ...@@ -35,11 +35,15 @@ export default {
isDirectMember() { isDirectMember() {
return this.member.source?.id === this.sourceId; return this.member.source?.id === this.sourceId;
}, },
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
},
}, },
render() { render() {
return this.$scopedSlots.default({ return this.$scopedSlots.default({
memberType: this.memberType, memberType: this.memberType,
isDirectMember: this.isDirectMember, 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 ...@@ -14,6 +14,8 @@ class ApplicationSetting
end end
def accepted_by_user?(user) def accepted_by_user?(user)
return true if user.project_bot?
user.accepted_term_id == id || user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists? term_agreements.accepted.where(user: user).exists?
end end
......
...@@ -1674,6 +1674,8 @@ class User < ApplicationRecord ...@@ -1674,6 +1674,8 @@ class User < ApplicationRecord
end end
def terms_accepted? def terms_accepted?
return true if project_bot?
accepted_term_id.present? accepted_term_id.present?
end 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: ...@@ -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, - You can control which jobs run in which cases, depending on how they are triggered,
with the [`rules` syntax](../yaml/README.md#rules). 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) - 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 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 permissions), and then any project can use them. This central project could also
......
...@@ -33,11 +33,8 @@ We have complete examples of configuring pipelines: ...@@ -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/) > - <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. > 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), 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 you may need to enable pipeline triggering. Go to your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
## Introduction ## Introduction
...@@ -961,7 +958,7 @@ GitLab performs a reverse deep merge based on the keys. GitLab: ...@@ -961,7 +958,7 @@ GitLab performs a reverse deep merge based on the keys. GitLab:
- Merges the `rspec` contents into `.tests` recursively. - Merges the `rspec` contents into `.tests` recursively.
- Doesn't merge the values of the keys. - 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 ```yaml
rspec: rspec:
...@@ -974,9 +971,6 @@ rspec: ...@@ -974,9 +971,6 @@ rspec:
- $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). 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 `.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 "" ...@@ -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." msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr "" msgstr ""
msgid "SAML"
msgstr ""
msgid "SAML SSO" msgid "SAML SSO"
msgstr "" msgstr ""
......
...@@ -5,6 +5,12 @@ module QA ...@@ -5,6 +5,12 @@ module QA
describe 'LDAP Group sync' do describe 'LDAP Group sync' do
include Support::Api 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 before(:all) do
# Create the sandbox group as the LDAP user. Without this the admin user # 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 # would own the sandbox group and then in subsequent tests the LDAP user
...@@ -62,10 +68,16 @@ module QA ...@@ -62,10 +68,16 @@ module QA
let(:owner_user) { 'enguser1' } let(:owner_user) { 'enguser1' }
let(:sync_users) { ['ENG User 2', 'ENG User 3'] } let(:sync_users) { ['ENG User 2', 'ENG User 3'] }
let(:group_name) { 'Synched-engineering-group' }
before do before do
@created_users = create_users_via_api(ldap_users) 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) 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) Page::Group::Menu.perform(&:go_to_ldap_sync_settings)
...@@ -113,12 +125,16 @@ module QA ...@@ -113,12 +125,16 @@ module QA
let(:owner_user) { 'hruser1' } let(:owner_user) { 'hruser1' }
let(:sync_users) { ['HR User 2', 'HR User 3'] } let(:sync_users) { ['HR User 2', 'HR User 3'] }
let(:group_name) { 'Synched-human-resources-group' }
before do 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) Page::Group::Menu.perform(&:go_to_ldap_sync_settings)
...@@ -160,23 +176,23 @@ module QA ...@@ -160,23 +176,23 @@ module QA
group group
end 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') user = Struct.new(:ldap_username, :ldap_password).new(user_name, 'password')
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform do |login_page| Page::Main::Login.perform do |login_page|
login_page.sign_in_using_ldap_credentials(user: user) login_page.sign_in_using_ldap_credentials(user: user)
end end
group.visit!
end end
def verify_users_synced(expected_users) def verify_users_synced(expected_users)
EE::Page::Group::Members.perform do |members| EE::Page::Group::Members.perform do |members|
members.click_sync_now members.click_sync_now
users_synchronised = members.retry_until(reload: true) do users_synchronised = members.retry_until(reload: true) do
expected_users.map { |user| members.has_content?(user) }.all? expected_users.map { |user| members.has_content?(user) }.all?
end end
expect(users_synchronised).to be_truthy expect(users_synchronised).to be_truthy
end end
end end
......
...@@ -26,6 +26,21 @@ RSpec.describe 'Users > Terms' do ...@@ -26,6 +26,21 @@ RSpec.describe 'Users > Terms' do
expect(page).not_to have_content('Continue') expect(page).not_to have_content('Continue')
end 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 context 'when signed in' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
import { mount, createWrapper } from '@vue/test-utils'; import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui'; import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { member, orphanedMember } from '../mock_data'; import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue'; import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
describe('MemberList', () => { describe('MemberList', () => {
let wrapper; let wrapper;
const { user } = member; const { user } = memberMock;
const createComponent = (propsData = {}) => { const createComponent = (propsData = {}) => {
wrapper = mount(UserAvatar, { wrapper = mount(UserAvatar, {
propsData: { propsData: {
member, member: memberMock,
isCurrentUser: false,
...propsData, ...propsData,
}, },
}); });
}; };
const getByText = (text, options) => const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options)); createWrapper(within(wrapper.element).findByText(text, options));
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -63,4 +64,25 @@ describe('MemberList', () => { ...@@ -63,4 +64,25 @@ describe('MemberList', () => {
expect(getByText('Orphaned member').exists()).toBe(true); 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', () => { ...@@ -11,7 +11,10 @@ describe('MemberList', () => {
const createComponent = propsData => { const createComponent = propsData => {
wrapper = shallowMount(MemberAvatar, { wrapper = shallowMount(MemberAvatar, {
propsData, propsData: {
isCurrentUser: false,
...propsData,
},
}); });
}; };
......
...@@ -15,6 +15,10 @@ describe('MemberList', () => { ...@@ -15,6 +15,10 @@ describe('MemberList', () => {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
}, },
render(createElement) { render(createElement) {
return createElement('div', this.memberType); return createElement('div', this.memberType);
...@@ -29,6 +33,7 @@ describe('MemberList', () => { ...@@ -29,6 +33,7 @@ describe('MemberList', () => {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
sourceId: 1, sourceId: 1,
currentUserId: 1,
...state, ...state,
}, },
}); });
...@@ -42,8 +47,13 @@ describe('MemberList', () => { ...@@ -42,8 +47,13 @@ describe('MemberList', () => {
propsData, propsData,
store: createStore(state), store: createStore(state),
scopedSlots: { scopedSlots: {
default: default: `
'<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />', <wrapped-component
:member-type="props.memberType"
:is-direct-member="props.isDirectMember"
:is-current-user="props.isCurrentUser"
/>
`,
}, },
}); });
}; };
...@@ -93,4 +103,28 @@ describe('MemberList', () => { ...@@ -93,4 +103,28 @@ describe('MemberList', () => {
expect(findWrappedComponent().props('isDirectMember')).toBe(false); 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 ...@@ -17,6 +17,7 @@ RSpec.describe ApplicationSetting::Term do
describe '#accepted_by_user?' do describe '#accepted_by_user?' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project_bot) { create(:user, :project_bot) }
let(:term) { create(:term) } let(:term) { create(:term) }
it 'is true when the user accepted the terms' do it 'is true when the user accepted the terms' do
...@@ -25,6 +26,10 @@ RSpec.describe ApplicationSetting::Term do ...@@ -25,6 +26,10 @@ RSpec.describe ApplicationSetting::Term do
expect(term.accepted_by_user?(user)).to be(true) expect(term.accepted_by_user?(user)).to be(true)
end 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 it 'is false when the user declined the terms' do
decline_terms(term, user) decline_terms(term, user)
......
...@@ -4330,28 +4330,32 @@ RSpec.describe User do ...@@ -4330,28 +4330,32 @@ RSpec.describe User do
describe '#required_terms_not_accepted?' do describe '#required_terms_not_accepted?' do
let(:user) { build(:user) } let(:user) { build(:user) }
let(:project_bot) { create(:user, :project_bot) }
subject { user.required_terms_not_accepted? } subject { user.required_terms_not_accepted? }
context "when terms are not enforced" do context "when terms are not enforced" do
it { is_expected.to be_falsy } it { is_expected.to be_falsey }
end end
context "when terms are enforced and accepted by the user" do context "when terms are enforced" do
before do before do
enforce_terms enforce_terms
accept_terms(user)
end end
it { is_expected.to be_falsy } it "is not accepted by the user" do
end expect(subject).to be_truthy
end
context "when terms are enforced but the user has not accepted" do it "is accepted by the user" do
before do accept_terms(user)
enforce_terms
expect(subject).to be_falsey
end 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
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