diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 09:09:59 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 09:09:59 +0300 |
commit | a7e81add72ecfbbf888ec4f73debdc2647b059de (patch) | |
tree | 3139b36451667bdd07181ef35044bd9903424fb1 /spec | |
parent | 4ed4dc08a806773e5dc326fc0c18bda6f6ea7af7 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
24 files changed, 456 insertions, 287 deletions
diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb index da35f246ea0..bcca48fb086 100644 --- a/spec/factories/packages/package_file.rb +++ b/spec/factories/packages/package_file.rb @@ -140,6 +140,14 @@ FactoryBot.define do size { 1149.bytes } end + trait(:generic) do + package + file_fixture { 'spec/fixtures/packages/generic/myfile.tar.gz' } + file_name { "#{package.name}.tar.gz" } + file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' } + size { 1149.bytes } + end + trait(:object_storage) do file_store { Packages::PackageFileUploader::Store::REMOTE } end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index ec30f34199d..304573ecd6e 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -184,4 +184,17 @@ RSpec.describe 'Group show page' do expect(page).to have_selector('.notifications-btn.disabled', visible: true) end end + + context 'page og:description' do + let(:group) { create(:group, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } + let(:maintainer) { create(:user) } + + before do + group.add_maintainer(maintainer) + sign_in(maintainer) + visit path + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end end diff --git a/spec/features/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb index 3f18764aa58..9b1c8be1513 100644 --- a/spec/features/issues/user_views_issue_spec.rb +++ b/spec/features/issues/user_views_issue_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" RSpec.describe "User views issue" do let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:user) { create(:user) } - let_it_be(:issue) { create(:issue, project: project, description: "# Description header", author: user) } + let_it_be(:issue) { create(:issue, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) } let_it_be(:note) { create(:note, noteable: issue, project: project, author: user) } before_all do @@ -20,6 +20,8 @@ RSpec.describe "User views issue" do it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") } + it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet' + it 'shows the merge request and issue actions', :aggregate_failures do expect(page).to have_link('New issue') expect(page).to have_button('Create merge request') diff --git a/spec/features/merge_request/user_sees_page_metadata_spec.rb b/spec/features/merge_request/user_sees_page_metadata_spec.rb new file mode 100644 index 00000000000..7b3e07152a0 --- /dev/null +++ b/spec/features/merge_request/user_sees_page_metadata_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Merge request > User sees page metadata' do + let(:merge_request) { create(:merge_request, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } + let(:project) { merge_request.target_project } + let(:user) { project.creator } + + before do + project.add_maintainer(user) + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' +end diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb index 11c6fa521d5..9c19f842427 100644 --- a/spec/features/milestones/user_views_milestone_spec.rb +++ b/spec/features/milestones/user_views_milestone_spec.rb @@ -6,7 +6,7 @@ RSpec.describe "User views milestone" do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, group: group) } - let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:milestone) { create(:milestone, project: project, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } let_it_be(:labels) { create_list(:label, 2, project: project) } before_all do @@ -17,6 +17,14 @@ RSpec.describe "User views milestone" do sign_in(user) end + context 'page description' do + before do + visit(project_milestone_path(project, milestone)) + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end + it "avoids N+1 database queries" do issue_params = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 970500985ae..6c1e1eab968 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -99,6 +99,15 @@ RSpec.describe 'Project' do expect(page).to have_css('.home-panel-description .is-expanded') end end + + context 'page description' do + before do + project.update_attribute(:description, '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') + visit path + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end end describe 'project topics' do diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb index dd5c2442d00..b3c8cf8d326 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'User page' do include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } + let(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') } context 'with public profile' do it 'shows all the tabs' do @@ -174,4 +174,12 @@ RSpec.describe 'User page' do end end end + + context 'page description' do + before do + visit(user_path(user)) + end + + it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' + end end diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index 5275845fe5b..7500f2fe59a 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -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) } diff --git a/spec/finders/packages/generic/package_finder_spec.rb b/spec/finders/packages/generic/package_finder_spec.rb new file mode 100644 index 00000000000..ed34268e7a9 --- /dev/null +++ b/spec/finders/packages/generic/package_finder_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Packages::Generic::PackageFinder do + let_it_be(:project) { create(:project) } + let_it_be(:package) { create(:generic_package, project: project) } + + describe '#execute!' do + subject(:finder) { described_class.new(project) } + + it 'finds package by name and version' do + found_package = finder.execute!(package.name, package.version) + + expect(found_package).to eq(package) + end + + it 'ignores packages with same name but different version' do + create(:generic_package, project: project, name: package.name, version: '3.1.4') + + found_package = finder.execute!(package.name, package.version) + + expect(found_package).to eq(package) + end + + it 'raises ActiveRecord::RecordNotFound if package is not found' do + expect { finder.execute!(package.name, '3.1.4') } + .to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js index 3a9a608b2e2..0e46693a4bf 100644 --- a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js +++ b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js @@ -28,8 +28,10 @@ describe('IngressModsecuritySettings', () => { }); }; - const findSaveButton = () => wrapper.find('.btn-success'); - const findCancelButton = () => wrapper.find('[variant="secondary"]'); + const findSaveButton = () => + wrapper.find('[data-qa-selector="save_ingress_modsecurity_settings"]'); + const findCancelButton = () => + wrapper.find('[data-qa-selector="cancel_ingress_modsecurity_settings"]'); const findModSecurityToggle = () => wrapper.find(GlToggle); const findModSecurityDropdown = () => wrapper.find(GlDeprecatedDropdown); diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js deleted file mode 100644 index 51b89f96ef2..00000000000 --- a/spec/frontend/registry/settings/store/actions_spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import Api from '~/api'; -import * as actions from '~/registry/settings/store/actions'; -import * as types from '~/registry/settings/store/mutation_types'; - -describe('Actions Registry Store', () => { - describe.each` - actionName | mutationName | payload - ${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'} - ${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'} - ${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined} - ${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined} - `( - '$actionName invokes $mutationName with payload $payload', - ({ actionName, mutationName, payload }) => { - it('should set state', done => { - testAction(actions[actionName], payload, {}, [{ type: mutationName, payload }], [], done); - }); - }, - ); - - describe('receiveSettingsSuccess', () => { - it('calls SET_SETTINGS', () => { - testAction( - actions.receiveSettingsSuccess, - 'foo', - {}, - [{ type: types.SET_SETTINGS, payload: 'foo' }], - [], - ); - }); - }); - - describe('fetchSettings', () => { - const state = { - projectId: 'bar', - }; - - const payload = { - data: { - container_expiration_policy: 'foo', - }, - }; - - it('should fetch the data from the API', done => { - Api.project = jest.fn().mockResolvedValue(payload); - testAction( - actions.fetchSettings, - null, - state, - [], - [ - { type: 'toggleLoading' }, - { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy }, - { type: 'toggleLoading' }, - ], - done, - ); - }); - }); - - describe('saveSettings', () => { - const state = { - projectId: 'bar', - settings: 'baz', - }; - - const payload = { - data: { - tag_expiration_policies: 'foo', - }, - }; - - it('should fetch the data from the API', done => { - Api.updateProject = jest.fn().mockResolvedValue(payload); - testAction( - actions.saveSettings, - null, - state, - [], - [ - { type: 'toggleLoading' }, - { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy }, - { type: 'toggleLoading' }, - ], - done, - ); - }); - }); -}); diff --git a/spec/frontend/registry/settings/store/getters_spec.js b/spec/frontend/registry/settings/store/getters_spec.js deleted file mode 100644 index b781d09466c..00000000000 --- a/spec/frontend/registry/settings/store/getters_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import * as getters from '~/registry/settings/store/getters'; -import * as utils from '~/registry/shared/utils'; -import { formOptions } from '../../shared/mock_data'; - -describe('Getters registry settings store', () => { - const settings = { - enabled: true, - cadence: 'foo', - keep_n: 'bar', - older_than: 'baz', - name_regex: 'name-foo', - name_regex_keep: 'name-keep-bar', - }; - - describe.each` - getter | variable | formOption - ${'getCadence'} | ${'cadence'} | ${'cadence'} - ${'getKeepN'} | ${'keep_n'} | ${'keepN'} - ${'getOlderThan'} | ${'older_than'} | ${'olderThan'} - `('Options getter', ({ getter, variable, formOption }) => { - beforeEach(() => { - utils.findDefaultOption = jest.fn(); - }); - - it(`${getter} returns ${variable} when ${variable} exists in settings`, () => { - expect(getters[getter]({ settings })).toBe(settings[variable]); - }); - - it(`${getter} calls findDefaultOption when ${variable} does not exists in settings`, () => { - getters[getter]({ settings: {}, formOptions }); - expect(utils.findDefaultOption).toHaveBeenCalledWith(formOptions[formOption]); - }); - }); - - describe('getSettings', () => { - it('returns the content of settings', () => { - const computedGetters = { - getCadence: settings.cadence, - getOlderThan: settings.older_than, - getKeepN: settings.keep_n, - }; - expect(getters.getSettings({ settings }, computedGetters)).toEqual(settings); - }); - }); - - describe('getIsEdited', () => { - it('returns false when original is equal to settings', () => { - const same = { foo: 'bar' }; - expect(getters.getIsEdited({ original: same, settings: same })).toBe(false); - }); - - it('returns true when original is different from settings', () => { - expect(getters.getIsEdited({ original: { foo: 'bar' }, settings: { foo: 'baz' } })).toBe( - true, - ); - }); - }); - - describe('getIsDisabled', () => { - it.each` - original | enableHistoricEntries | result - ${undefined} | ${false} | ${true} - ${{ foo: 'bar' }} | ${undefined} | ${false} - ${{}} | ${false} | ${false} - `( - 'returns $result when original is $original and enableHistoricEntries is $enableHistoricEntries', - ({ original, enableHistoricEntries, result }) => { - expect(getters.getIsDisabled({ original, enableHistoricEntries })).toBe(result); - }, - ); - }); -}); diff --git a/spec/frontend/registry/settings/store/mutations_spec.js b/spec/frontend/registry/settings/store/mutations_spec.js deleted file mode 100644 index 1d85e38eb36..00000000000 --- a/spec/frontend/registry/settings/store/mutations_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import mutations from '~/registry/settings/store/mutations'; -import * as types from '~/registry/settings/store/mutation_types'; -import createState from '~/registry/settings/store/state'; -import { formOptions, stringifiedFormOptions } from '../../shared/mock_data'; - -describe('Mutations Registry Store', () => { - let mockState; - - beforeEach(() => { - mockState = createState(); - }); - - describe('SET_INITIAL_STATE', () => { - it('should set the initial state', () => { - const payload = { - projectId: 'foo', - enableHistoricEntries: false, - adminSettingsPath: 'foo', - isAdmin: true, - }; - const expectedState = { ...mockState, ...payload, formOptions }; - mutations[types.SET_INITIAL_STATE](mockState, { - ...payload, - ...stringifiedFormOptions, - }); - - expect(mockState).toEqual(expectedState); - }); - }); - - describe('UPDATE_SETTINGS', () => { - it('should update the settings', () => { - mockState.settings = { foo: 'bar' }; - const payload = { foo: 'baz' }; - const expectedState = { ...mockState, settings: payload }; - mutations[types.UPDATE_SETTINGS](mockState, { settings: payload }); - expect(mockState.settings).toEqual(expectedState.settings); - }); - }); - - describe('SET_SETTINGS', () => { - it('should set the settings and original', () => { - const payload = { foo: 'baz' }; - const expectedState = { ...mockState, settings: payload }; - mutations[types.SET_SETTINGS](mockState, payload); - expect(mockState.settings).toEqual(expectedState.settings); - expect(mockState.original).toEqual(expectedState.settings); - }); - - it('should keep the default state when settings is not present', () => { - const originalSettings = { ...mockState.settings }; - mutations[types.SET_SETTINGS](mockState); - expect(mockState.settings).toEqual(originalSettings); - expect(mockState.original).toEqual(undefined); - }); - }); - - describe('RESET_SETTINGS', () => { - it('should copy original over settings', () => { - mockState.settings = { foo: 'bar' }; - mockState.original = { foo: 'baz' }; - mutations[types.RESET_SETTINGS](mockState); - expect(mockState.settings).toEqual(mockState.original); - }); - - it('if original is undefined it should initialize to empty object', () => { - mockState.settings = { foo: 'bar' }; - mockState.original = undefined; - mutations[types.RESET_SETTINGS](mockState); - expect(mockState.settings).toEqual({}); - }); - }); - - describe('TOGGLE_LOADING', () => { - it('should toggle the loading', () => { - mutations[types.TOGGLE_LOADING](mockState); - expect(mockState.isLoading).toEqual(true); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js index f688e5789cb..79d5129b5ef 100644 --- a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js +++ b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js @@ -1,25 +1,26 @@ 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); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js index cf3b28f7bf6..a171dd830c1 100644 --- a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js +++ b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js @@ -11,7 +11,10 @@ describe('MemberList', () => { const createComponent = propsData => { wrapper = shallowMount(MemberAvatar, { - propsData, + propsData: { + isCurrentUser: false, + ...propsData, + }, }); }; diff --git a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js index 372e07d5e27..960d9bc164c 100644 --- a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js +++ b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js @@ -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); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js new file mode 100644 index 00000000000..f183abc08d6 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/utils_spec.js @@ -0,0 +1,29 @@ +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)); + }); + }); +}); diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index d1fde517488..704a4e7b224 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -599,6 +599,20 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('') } end + describe '.generic_package_name_regex' do + subject { described_class.generic_package_name_regex } + + it { is_expected.to match('123') } + it { is_expected.to match('foo') } + it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1') } + it { is_expected.not_to match('../../foo') } + it { is_expected.not_to match('..\..\foo') } + it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') } + it { is_expected.not_to match('$foo/bar') } + it { is_expected.not_to match('my file name') } + it { is_expected.not_to match('!!()()') } + end + describe '.generic_package_file_name_regex' do subject { described_class.generic_package_file_name_regex } diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb index 82347453437..51a6027698f 100644 --- a/spec/models/application_setting/term_spec.rb +++ b/spec/models/application_setting/term_spec.rb @@ -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) diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index a66e96d8a19..26851c93ac3 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Ci::JobArtifact do let!(:metrics_report) { create(:ci_job_artifact, :junit) } let!(:codequality_report) { create(:ci_job_artifact, :codequality) } - it { is_expected.to eq([metrics_report, codequality_report]) } + it { is_expected.to match_array([metrics_report, codequality_report]) } end end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index ea1f75d04e7..6a3969802f3 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -108,6 +108,20 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('.foobar').for(:name) } it { is_expected.not_to allow_value('%foo%bar').for(:name) } end + + context 'generic package' do + subject { build_stubbed(:generic_package) } + + it { is_expected.to allow_value('123').for(:name) } + it { is_expected.to allow_value('foo').for(:name) } + it { is_expected.to allow_value('foo.bar.baz-2.0-20190901.47283-1').for(:name) } + it { is_expected.not_to allow_value('../../foo').for(:name) } + it { is_expected.not_to allow_value('..\..\foo').for(:name) } + it { is_expected.not_to allow_value('%2f%2e%2e%2f%2essh%2fauthorized_keys').for(:name) } + it { is_expected.not_to allow_value('$foo/bar').for(:name) } + it { is_expected.not_to allow_value('my file name').for(:name) } + it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) } + end end describe '#version' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1841288cd4b..0f71c7790d4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index bebeed9402e..2cb686167f1 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -33,7 +33,19 @@ RSpec.describe API::GenericPackages do { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token } end - describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize' do + shared_examples 'secure endpoint' do + before do + project.add_developer(user) + end + + it 'rejects malicious request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do context 'with valid project' do using RSpec::Parameterized::TableSyntax @@ -73,41 +85,49 @@ RSpec.describe API::GenericPackages do end it "responds with #{params[:expected_status]}" do - headers = workhorse_header.merge(auth_header) - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize" - - put api(url), headers: headers + authorize_upload_file(workhorse_header.merge(auth_header)) expect(response).to have_gitlab_http_status(expected_status) end end end - it 'rejects a malicious request' do - project.add_developer(user) - headers = workhorse_header.merge(personal_access_token_header) - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/%2e%2e%2f.ssh%2fauthorized_keys/authorize" + context 'application security' do + using RSpec::Parameterized::TableSyntax - put api(url), headers: headers + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end - expect(response).to have_gitlab_http_status(:bad_request) + with_them do + subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' + end end context 'generic_packages feature flag is disabled' do it 'responds with 404 Not Found' do stub_feature_flags(generic_packages: false) project.add_developer(user) - headers = workhorse_header.merge(personal_access_token_header) - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize" - put api(url), headers: headers + authorize_upload_file(workhorse_header.merge(personal_access_token_header)) expect(response).to have_gitlab_http_status(:not_found) end end + + def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}/authorize" + + put api(url), headers: request_headers + end end - describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz' do + describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do include WorkhorseHelpers let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') } @@ -246,17 +266,27 @@ RSpec.describe API::GenericPackages do expect(response).to have_gitlab_http_status(:forbidden) end + end - it 'rejects a malicious request' do - headers = workhorse_header.merge(personal_access_token_header) - upload_file(params, headers, file_name: '%2e%2e%2f.ssh%2fauthorized_keys') + context 'application security' do + using RSpec::Parameterized::TableSyntax - expect(response).to have_gitlab_http_status(:bad_request) + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) } + + it_behaves_like 'secure endpoint' end end - def upload_file(params, request_headers, send_rewritten_field: true, file_name: 'myfile.tar.gz') - url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/#{file_name}" + def upload_file(params, request_headers, send_rewritten_field: true, package_name: 'mypackage', file_name: 'myfile.tar.gz') + url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}" workhorse_finalize( api(url), @@ -268,4 +298,138 @@ RSpec.describe API::GenericPackages do ) end end + + describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:package) { create(:generic_package, project: project) } + let_it_be(:package_file) { create(:package_file, :generic, package: package) } + + context 'authentication' do + where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do + 'PUBLIC' | :developer | true | :personal_access_token | :success + 'PUBLIC' | :guest | true | :personal_access_token | :success + 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :developer | false | :personal_access_token | :success + 'PUBLIC' | :guest | false | :personal_access_token | :success + 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PUBLIC' | :anonymous | false | :none | :unauthorized + 'PRIVATE' | :developer | true | :personal_access_token | :success + 'PRIVATE' | :guest | true | :personal_access_token | :forbidden + 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :developer | false | :personal_access_token | :not_found + 'PRIVATE' | :guest | false | :personal_access_token | :not_found + 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized + 'PRIVATE' | :anonymous | false | :none | :unauthorized + 'PUBLIC' | :developer | true | :job_token | :success + 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized + 'PUBLIC' | :developer | false | :job_token | :success + 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | true | :job_token | :success + 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized + 'PRIVATE' | :developer | false | :job_token | :not_found + 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.send("add_#{user_role}", user) if member? && user_role != :anonymous + end + + it "responds with #{params[:expected_status]}" do + download_file(auth_header) + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'event tracking' do + before do + project.add_developer(user) + end + + subject { download_file(personal_access_token_header) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '../.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious file name request' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: '%2e%2e%2f.ssh%2fauthorized_keys') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package/../') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects a malicious package name request' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'my-package%2f%2e%2e%2f') + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'application security' do + using RSpec::Parameterized::TableSyntax + + where(:param_name, :param_value) do + :package_name | 'my-package/../' + :package_name | 'my-package%2f%2e%2e%2f' + :file_name | '../.ssh%2fauthorized_keys' + :file_name | '%2e%2e%2f.ssh%2fauthorized_keys' + end + + with_them do + subject { download_file(personal_access_token_header, param_name => param_value) } + + it_behaves_like 'secure endpoint' + end + end + + it 'responds with 404 Not Found for non existing package' do + project.add_developer(user) + + download_file(personal_access_token_header, package_name: 'no-such-package') + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'responds with 404 Not Found for non existing package file' do + project.add_developer(user) + + download_file(personal_access_token_header, file_name: 'no-such-file') + + expect(response).to have_gitlab_http_status(:not_found) + end + + def download_file(request_headers, package_name: nil, file_name: nil) + package_name ||= package.name + file_name ||= package_file.file_name + url = "/projects/#{project.id}/packages/generic/#{package_name}/#{package.version}/#{file_name}" + + get api(url), headers: request_headers + end + end end diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb new file mode 100644 index 00000000000..81653220b4c --- /dev/null +++ b/spec/support/shared_examples/features/page_description_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'page meta description' do |expected_description| + it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do + %w(name='description' property='og:description' property='twitter:description').each do |selector| + expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false) + end + end +end |