diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-06 12:10:29 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-06 12:10:29 +0000 |
commit | 5564275a0b378298dc6281599cbfe71a937109ff (patch) | |
tree | a468e1e60046356410219c35c23a8a428c5e2c5e /spec | |
parent | d87918510a866a5fcbbc2f899ad65c6938ebf5f5 (diff) | |
download | gitlab-ce-5564275a0b378298dc6281599cbfe71a937109ff.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
30 files changed, 1142 insertions, 64 deletions
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 77e7b32af25..98a9c3eaec6 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -47,6 +47,7 @@ describe Profiles::PreferencesController do theme_id: '2', first_day_of_week: '1', preferred_language: 'jp', + tab_width: '5', render_whitespace_in_code: 'true' }.with_indifferent_access diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb index 42ed66ac191..e86d4ab8812 100644 --- a/spec/factories/deploy_tokens.rb +++ b/spec/factories/deploy_tokens.rb @@ -9,6 +9,7 @@ FactoryBot.define do read_registry { true } revoked { false } expires_at { 5.days.from_now } + deploy_token_type { DeployToken.deploy_token_types[:project_type] } trait :revoked do revoked { true } @@ -21,5 +22,13 @@ FactoryBot.define do trait :expired do expires_at { Date.today - 1.month } end + + trait :group do + deploy_token_type { DeployToken.deploy_token_types[:group_type] } + end + + trait :project do + deploy_token_type { DeployToken.deploy_token_types[:project_type] } + end end end diff --git a/spec/factories/group_deploy_tokens.rb b/spec/factories/group_deploy_tokens.rb new file mode 100644 index 00000000000..9ec7d0701be --- /dev/null +++ b/spec/factories/group_deploy_tokens.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :group_deploy_token do + group + deploy_token + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index f83c137b758..34f6da682b6 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -23,6 +23,10 @@ FactoryBot.define do after(:build) { |user, _| user.block! } end + trait :bot do + bot_type { User.bot_types[:alert_bot] } + end + trait :external do external { true } end diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb index 5662465d431..8c16dcec42f 100644 --- a/spec/features/groups/navbar_spec.rb +++ b/spec/features/groups/navbar_spec.rb @@ -3,53 +3,53 @@ require 'spec_helper' describe 'Group navbar' do - it_behaves_like 'verified navigation bar' do - let(:user) { create(:user) } - let(:group) { create(:group) } + let(:user) { create(:user) } + let(:group) { create(:group) } + + let(:analytics_nav_item) do + { + nav_item: _('Analytics'), + nav_sub_items: [ + _('Contribution Analytics') + ] + } + end - let(:analytics_nav_item) do + let(:structure) do + [ + { + nav_item: _('Group overview'), + nav_sub_items: [ + _('Details'), + _('Activity') + ] + }, { - nav_item: _('Analytics'), + nav_item: _('Issues'), nav_sub_items: [ - _('Contribution Analytics') + _('List'), + _('Board'), + _('Labels'), + _('Milestones') ] + }, + { + nav_item: _('Merge Requests'), + nav_sub_items: [] + }, + { + nav_item: _('Kubernetes'), + nav_sub_items: [] + }, + (analytics_nav_item if Gitlab.ee?), + { + nav_item: _('Members'), + nav_sub_items: [] } - end - - let(:structure) do - [ - { - nav_item: _('Group overview'), - nav_sub_items: [ - _('Details'), - _('Activity') - ] - }, - { - nav_item: _('Issues'), - nav_sub_items: [ - _('List'), - _('Board'), - _('Labels'), - _('Milestones') - ] - }, - { - nav_item: _('Merge Requests'), - nav_sub_items: [] - }, - { - nav_item: _('Kubernetes'), - nav_sub_items: [] - }, - (analytics_nav_item if Gitlab.ee?), - { - nav_item: _('Members'), - nav_sub_items: [] - } - ] - end + ] + end + it_behaves_like 'verified navigation bar' do before do group.add_maintainer(user) sign_in(user) @@ -57,4 +57,21 @@ describe 'Group navbar' do visit group_path(group) end end + + if Gitlab.ee? + context 'when productivity analytics is available' do + before do + stub_licensed_features(productivity_analytics: true) + + analytics_nav_item[:nav_sub_items] << _('Productivity Analytics') + + group.add_maintainer(user) + sign_in(user) + + visit group_path(group) + end + + it_behaves_like 'verified navigation bar' + end + end end diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb index 4f2c5fc73d8..17ff494a6fa 100644 --- a/spec/features/merge_request/maintainer_edits_fork_spec.rb +++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb @@ -20,7 +20,7 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js end before do - stub_feature_flags(web_ide_default: false, single_mr_diff_view: false) + stub_feature_flags(web_ide_default: false, single_mr_diff_view: false, code_navigation: false) target_project.add_maintainer(user) sign_in(user) diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb index 2d2da222998..6e61536d5ff 100644 --- a/spec/features/profiles/user_edit_preferences_spec.rb +++ b/spec/features/profiles/user_edit_preferences_spec.rb @@ -29,4 +29,31 @@ describe 'User edit preferences profile' do expect(field).not_to be_checked end + + describe 'User changes tab width to acceptable value' do + it 'shows success message' do + fill_in 'Tab width', with: 9 + click_button 'Save changes' + + expect(page).to have_content('Preferences saved.') + end + + it 'saves the value' do + tab_width_field = page.find_field('Tab width') + + expect do + tab_width_field.fill_in with: 6 + click_button 'Save changes' + end.to change { tab_width_field.value } + end + end + + describe 'User changes tab width to unacceptable value' do + it 'shows error message' do + fill_in 'Tab width', with: -1 + click_button 'Save changes' + + expect(page).to have_content('Failed to save preferences') + end + end end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 5d86e4125df..e714d0f7cad 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -13,6 +13,10 @@ describe 'File blob', :js do wait_for_requests end + before do + stub_feature_flags(code_navigation: false) + end + context 'Ruby file' do before do visit_blob('files/ruby/popen.rb') diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index a1d6a8896c7..5d62b2f87bb 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -69,6 +69,8 @@ describe 'Editing file blob', :js do context 'from blob file path' do before do + stub_feature_flags(code_navigation: false) + visit project_blob_path(project, tree_join(branch, file_path)) end diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb index b90129d6176..30878b7fb64 100644 --- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb +++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -8,6 +8,7 @@ describe 'User creates blob in new project', :js do shared_examples 'creating a file' do before do + stub_feature_flags(code_navigation: false) sign_in(user) visit project_path(project) end diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index eb9a4d8cb09..2d4f22e299e 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -14,7 +14,7 @@ describe 'Projects > Files > User creates files', :js do let(:user) { create(:user) } before do - stub_feature_flags(web_ide_default: false) + stub_feature_flags(web_ide_default: false, code_navigation: false) project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 0f543e47631..5e36407d9cb 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -14,6 +14,8 @@ describe 'Projects > Files > User deletes files', :js do let(:user) { create(:user) } before do + stub_feature_flags(code_navigation: false) + sign_in(user) end diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index 4c54bbdcd67..e1eefdcc40f 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -16,6 +16,8 @@ describe 'Projects > Files > User replaces files', :js do let(:user) { create(:user) } before do + stub_feature_flags(code_navigation: false) + sign_in(user) end diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap new file mode 100644 index 00000000000..dda6d68018e --- /dev/null +++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Code navigation popover component renders popover 1`] = ` +<div + class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" + style="left: 0px; top: 0px;" +> + <div + class="arrow" + style="left: 0px;" + /> + + <div + class="border-bottom" + > + <pre + class="border-0 bg-transparent m-0 code highlight" + > + console.log + </pre> + </div> + + <div + class="popover-body" + > + <gl-button-stub + class="w-100" + href="http://test.com" + size="md" + target="_blank" + variant="default" + > + + Go to definition + + </gl-button-stub> + </div> +</div> +`; diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js new file mode 100644 index 00000000000..cfdc0dcc6cc --- /dev/null +++ b/spec/frontend/code_navigation/components/app_spec.js @@ -0,0 +1,64 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import createState from '~/code_navigation/store/state'; +import App from '~/code_navigation/components/app.vue'; +import Popover from '~/code_navigation/components/popover.vue'; + +const localVue = createLocalVue(); +const fetchData = jest.fn(); +const showDefinition = jest.fn(); +let wrapper; + +localVue.use(Vuex); + +function factory(initialState = {}) { + const store = new Vuex.Store({ + state: { + ...createState(), + ...initialState, + }, + actions: { + fetchData, + showDefinition, + }, + }); + + wrapper = shallowMount(App, { store, localVue }); +} + +describe('Code navigation app component', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('fetches data on mount', () => { + factory(); + + expect(fetchData).toHaveBeenCalled(); + }); + + it('hides popover when no definition set', () => { + factory(); + + expect(wrapper.find(Popover).exists()).toBe(false); + }); + + it('renders popover when definition set', () => { + factory({ + currentDefinition: { hover: 'console' }, + currentDefinitionPosition: { x: 0 }, + }); + + expect(wrapper.find(Popover).exists()).toBe(true); + }); + + it('calls showDefinition when clicking blob viewer', () => { + setFixtures('<div class="blob-viewer"></div>'); + + factory(); + + document.querySelector('.blob-viewer').click(); + + expect(showDefinition).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js new file mode 100644 index 00000000000..ad05504a224 --- /dev/null +++ b/spec/frontend/code_navigation/components/popover_spec.js @@ -0,0 +1,58 @@ +import { shallowMount } from '@vue/test-utils'; +import Popover from '~/code_navigation/components/popover.vue'; + +const MOCK_CODE_DATA = Object.freeze({ + hover: [ + { + language: 'javascript', + value: 'console.log', + }, + ], + definition_url: 'http://test.com', +}); + +const MOCK_DOCS_DATA = Object.freeze({ + hover: [ + { + language: null, + value: 'console.log', + }, + ], + definition_url: 'http://test.com', +}); + +let wrapper; + +function factory(position, data) { + wrapper = shallowMount(Popover, { propsData: { position, data } }); +} + +describe('Code navigation popover component', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('renders popover', () => { + factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); + + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('code output', () => { + it('renders code output', () => { + factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA); + + expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true); + expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false); + }); + }); + + describe('documentation output', () => { + it('renders code output', () => { + factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA); + + expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false); + expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js new file mode 100644 index 00000000000..5e29a76f804 --- /dev/null +++ b/spec/frontend/code_navigation/store/actions_spec.js @@ -0,0 +1,221 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import actions from '~/code_navigation/store/actions'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils'; + +jest.mock('~/flash'); +jest.mock('~/code_navigation/utils'); + +describe('Code navigation actions', () => { + describe('setInitialData', () => { + it('commits SET_INITIAL_DATA', done => { + testAction( + actions.setInitialData, + { projectPath: 'test' }, + {}, + [{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test' } }], + [], + done, + ); + }); + }); + + describe('requestDataError', () => { + it('commits REQUEST_DATA_ERROR', () => + testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], [])); + + it('creates a flash message', () => + testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []).then( + () => { + expect(createFlash).toHaveBeenCalled(); + }, + )); + }); + + describe('fetchData', () => { + let mock; + const state = { + projectPath: 'gitlab-org/gitlab', + commitId: '123', + blobPath: 'index', + }; + const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info'; + + beforeEach(() => { + window.gon = { api_version: '1' }; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(apiUrl).replyOnce(200, [ + { + start_line: 0, + start_char: 0, + hover: { value: '123' }, + }, + { + start_line: 1, + start_char: 0, + hover: null, + }, + ]); + }); + + it('commits REQUEST_DATA_SUCCESS with normalized data', done => { + testAction( + actions.fetchData, + null, + state, + [ + { type: 'REQUEST_DATA' }, + { + type: 'REQUEST_DATA_SUCCESS', + payload: { '0:0': { start_line: 0, start_char: 0, hover: { value: '123' } } }, + }, + ], + [], + done, + ); + }); + + it('calls addInteractionClass with data', done => { + testAction( + actions.fetchData, + null, + state, + [ + { type: 'REQUEST_DATA' }, + { + type: 'REQUEST_DATA_SUCCESS', + payload: { '0:0': { start_line: 0, start_char: 0, hover: { value: '123' } } }, + }, + ], + [], + ) + .then(() => { + expect(addInteractionClass).toHaveBeenCalledWith({ + start_line: 0, + start_char: 0, + hover: { value: '123' }, + }); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(apiUrl).replyOnce(500); + }); + + it('dispatches requestDataError', done => { + testAction( + actions.fetchData, + null, + state, + [{ type: 'REQUEST_DATA' }], + [{ type: 'requestDataError' }], + done, + ); + }); + }); + }); + + describe('showDefinition', () => { + let target; + + beforeEach(() => { + target = document.createElement('div'); + }); + + it('returns early when no data exists', done => { + testAction(actions.showDefinition, { target }, {}, [], [], done); + }); + + it('commits SET_CURRENT_DEFINITION when target is not code navitation element', done => { + testAction( + actions.showDefinition, + { target }, + { data: {} }, + [ + { + type: 'SET_CURRENT_DEFINITION', + payload: { definition: undefined, position: undefined }, + }, + ], + [], + done, + ); + }); + + it('commits SET_CURRENT_DEFINITION with LSIF data', done => { + target.classList.add('js-code-navigation'); + target.setAttribute('data-line-index', '0'); + target.setAttribute('data-char-index', '0'); + + testAction( + actions.showDefinition, + { target }, + { data: { '0:0': { hover: 'test' } } }, + [ + { + type: 'SET_CURRENT_DEFINITION', + payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } }, + }, + ], + [], + done, + ); + }); + + it('adds hll class to target element', () => { + target.classList.add('js-code-navigation'); + target.setAttribute('data-line-index', '0'); + target.setAttribute('data-char-index', '0'); + + return testAction( + actions.showDefinition, + { target }, + { data: { '0:0': { hover: 'test' } } }, + [ + { + type: 'SET_CURRENT_DEFINITION', + payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } }, + }, + ], + [], + ).then(() => { + expect(target.classList).toContain('hll'); + }); + }); + + it('caches current target element', () => { + target.classList.add('js-code-navigation'); + target.setAttribute('data-line-index', '0'); + target.setAttribute('data-char-index', '0'); + + return testAction( + actions.showDefinition, + { target }, + { data: { '0:0': { hover: 'test' } } }, + [ + { + type: 'SET_CURRENT_DEFINITION', + payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } }, + }, + ], + [], + ).then(() => { + expect(setCurrentHoverElement).toHaveBeenCalledWith(target); + }); + }); + }); +}); diff --git a/spec/frontend/code_navigation/store/mutations_spec.js b/spec/frontend/code_navigation/store/mutations_spec.js new file mode 100644 index 00000000000..117a2ed2f14 --- /dev/null +++ b/spec/frontend/code_navigation/store/mutations_spec.js @@ -0,0 +1,63 @@ +import mutations from '~/code_navigation/store/mutations'; +import createState from '~/code_navigation/store/state'; + +let state; + +describe('Code navigation mutations', () => { + beforeEach(() => { + state = createState(); + }); + + describe('SET_INITIAL_DATA', () => { + it('sets initial data', () => { + mutations.SET_INITIAL_DATA(state, { + projectPath: 'test', + commitId: '123', + blobPath: 'index.js', + }); + + expect(state.projectPath).toBe('test'); + expect(state.commitId).toBe('123'); + expect(state.blobPath).toBe('index.js'); + }); + }); + + describe('REQUEST_DATA', () => { + it('sets loading true', () => { + mutations.REQUEST_DATA(state); + + expect(state.loading).toBe(true); + }); + }); + + describe('REQUEST_DATA_SUCCESS', () => { + it('sets loading false', () => { + mutations.REQUEST_DATA_SUCCESS(state, ['test']); + + expect(state.loading).toBe(false); + }); + + it('sets data', () => { + mutations.REQUEST_DATA_SUCCESS(state, ['test']); + + expect(state.data).toEqual(['test']); + }); + }); + + describe('REQUEST_DATA_ERROR', () => { + it('sets loading false', () => { + mutations.REQUEST_DATA_ERROR(state); + + expect(state.loading).toBe(false); + }); + }); + + describe('SET_CURRENT_DEFINITION', () => { + it('sets current definition and position', () => { + mutations.SET_CURRENT_DEFINITION(state, { definition: 'test', position: { x: 0 } }); + + expect(state.currentDefinition).toBe('test'); + expect(state.currentDefinitionPosition).toEqual({ x: 0 }); + }); + }); +}); diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js new file mode 100644 index 00000000000..458cc536635 --- /dev/null +++ b/spec/frontend/code_navigation/utils/index_spec.js @@ -0,0 +1,58 @@ +import { + cachedData, + getCurrentHoverElement, + setCurrentHoverElement, + addInteractionClass, +} from '~/code_navigation/utils'; + +afterEach(() => { + if (cachedData.has('current')) { + cachedData.delete('current'); + } +}); + +describe('getCurrentHoverElement', () => { + it.each` + value + ${'test'} + ${undefined} + `('it returns cached current key', ({ value }) => { + if (value) { + cachedData.set('current', value); + } + + expect(getCurrentHoverElement()).toEqual(value); + }); +}); + +describe('setCurrentHoverElement', () => { + it('sets cached current key', () => { + setCurrentHoverElement('test'); + + expect(getCurrentHoverElement()).toEqual('test'); + }); +}); + +describe('addInteractionClass', () => { + beforeEach(() => { + setFixtures( + '<div id="LC1"><span>console</span><span>.</span><span>log</span></div><div id="LC2"><span>function</span></div>', + ); + }); + + it.each` + line | char | index + ${0} | ${0} | ${0} + ${0} | ${8} | ${2} + ${1} | ${0} | ${0} + `( + 'it sets code navigation attributes for line $line and character $char', + ({ line, char, index }) => { + addInteractionClass({ start_line: line, start_char: char }); + + expect(document.querySelectorAll(`#LC${line + 1} span`)[index].classList).toContain( + 'js-code-navigation', + ); + }, + ); +}); diff --git a/spec/initializers/mail_encoding_patch_spec.rb b/spec/initializers/mail_encoding_patch_spec.rb new file mode 100644 index 00000000000..41074af3503 --- /dev/null +++ b/spec/initializers/mail_encoding_patch_spec.rb @@ -0,0 +1,207 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +require 'mail' +require_relative '../../config/initializers/mail_encoding_patch.rb' + +describe 'Mail quoted-printable transfer encoding patch and Unicode characters' do + shared_examples 'email encoding' do |email| + it 'enclosing in a new object does not change the encoded original' do + new_email = Mail.new(email) + + expect(new_email.subject).to eq(email.subject) + expect(new_email.from).to eq(email.from) + expect(new_email.to).to eq(email.to) + expect(new_email.content_type).to eq(email.content_type) + expect(new_email.content_transfer_encoding).to eq(email.content_transfer_encoding) + + expect(new_email.encoded).to eq(email.encoded) + end + end + + context 'with a text email' do + context 'with a body that encodes to exactly 74 characters (final newline)' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/plain; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-1\n" + end + + it_behaves_like 'email encoding', email + end + + context 'with a body that encodes to exactly 74 characters (no final newline)' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/plain; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-12" + end + + it_behaves_like 'email encoding', email + end + + context 'with a body that encodes to exactly 75 characters' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/plain; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-12\n" + end + + it_behaves_like 'email encoding', email + end + end + + context 'with an html email' do + context 'with a body that encodes to exactly 74 characters (final newline)' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/html; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-1234</p>\n" + end + + it_behaves_like 'email encoding', email + end + + context 'with a body that encodes to exactly 74 characters (no final newline)' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/html; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-12345</p>" + end + + it_behaves_like 'email encoding', email + end + + context 'with a body that encodes to exactly 75 characters' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/html; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-12345</p>\n" + end + + it_behaves_like 'email encoding', email + end + end + + context 'a multipart email' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + end + + text_part = Mail::Part.new do + content_type 'text/plain; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n" + end + + html_part = Mail::Part.new do + content_type 'text/html; charset=UTF-8' + content_transfer_encoding 'quoted-printable' + body "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n" + end + + email.text_part = text_part + email.html_part = html_part + + it_behaves_like 'email encoding', email + end + + context 'with non UTF-8 charset' do + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + content_type 'text/plain; charset=windows-1251' + content_transfer_encoding 'quoted-printable' + body "This line is very long and will be put in multiple quoted-printable lines. Some Russian character: Д\n\n\n".encode('windows-1251') + end + + it_behaves_like 'email encoding', email + + it 'can be decoded back' do + expect(Mail.new(email).body.decoded.dup.force_encoding('windows-1251').encode('utf-8')).to include('Some Russian character: Д') + end + end + + context 'with binary content' do + context 'can be encoded with \'base64\' content-transfer-encoding' do + image = File.binread('spec/fixtures/rails_sample.jpg') + + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + end + + part = Mail::Part.new + part.body = [image].pack('m') + part.content_type = 'image/jpg' + part.content_transfer_encoding = 'base64' + + email.parts << part + + it_behaves_like 'email encoding', email + + it 'binary contents are not modified' do + expect(email.parts.first.decoded).to eq(image) + + # Enclosing in a new Mail object does not corrupt encoded data + expect(Mail.new(email).parts.first.decoded).to eq(image) + end + end + + context 'encoding fails with \'quoted-printable\' content-transfer-encoding' do + image = File.binread('spec/fixtures/rails_sample.jpg') + + email = Mail.new do + to 'jane.doe@example.com' + from 'John Dóe <john.doe@example.com>' + subject 'Encoding tést' + end + + part = Mail::Part.new + part.body = [image].pack('M*') + part.content_type = 'image/jpg' + part.content_transfer_encoding = 'quoted-printable' + + email.parts << part + + # The Mail patch in `config/initializers/mail_encoding_patch.rb` fixes + # encoding of non-binary content. The failure below is expected since we + # reverted some upstream changes in order to properly support SMIME signatures + # See https://gitlab.com/gitlab-org/gitlab/issues/197386 + it 'content cannot be decoded back' do + # Headers are ok + expect(email.subject).to eq(email.subject) + expect(email.from).to eq(email.from) + expect(email.to).to eq(email.to) + expect(email.content_type).to eq(email.content_type) + expect(email.content_transfer_encoding).to eq(email.content_transfer_encoding) + + # Content cannot be recovered + expect(email.parts.first.decoded).not_to eq(image) + end + end + end +end diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/javascripts/reports/components/modal_spec.js index d42c509e5b5..ff046e64b6e 100644 --- a/spec/javascripts/reports/components/modal_spec.js +++ b/spec/javascripts/reports/components/modal_spec.js @@ -42,8 +42,8 @@ describe('Grouped Test Reports Modal', () => { ); }); - it('renders miliseconds', () => { - expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} ms`); + it('renders seconds', () => { + expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} s`); }); it('render title', () => { diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js index 3962f837a00..b3def474957 100644 --- a/spec/javascripts/user_popovers_spec.js +++ b/spec/javascripts/user_popovers_spec.js @@ -38,6 +38,13 @@ describe('User Popovers', () => { expect(document.querySelectorAll(selector).length).toBe(popovers.length); }); + it('does not initialize the user popovers twice for the same element', () => { + const newPopovers = initUserPopovers(document.querySelectorAll(selector)); + const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover); + + expect(samePopovers).toBe(true); + }); + describe('when user link emits mouseenter event', () => { let userLink; diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 1f943bebbec..ed763f63756 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -460,6 +460,20 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end end + context 'when the deploy token is of group type' do + let(:project_with_group) { create(:project, group: create(:group)) } + let(:deploy_token) { create(:deploy_token, :group, read_repository: true, groups: [project_with_group.group]) } + let(:login) { deploy_token.username } + + subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, ip: 'ip') } + + it 'succeeds when login and a group deploy token are valid' do + auth_success = Gitlab::Auth::Result.new(deploy_token, project_with_group, :deploy_token, [:download_code, :read_container_image]) + + expect(subject).to eq(auth_success) + end + end + context 'when the deploy token has read_registry as a scope' do let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) } let(:login) { deploy_token.username } @@ -469,10 +483,10 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do stub_container_registry_config(enabled: true) end - it 'succeeds when login and token are valid' do + it 'succeeds when login and a project token are valid' do auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image]) - expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip')) + expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip')) .to eq(auth_success) end diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb index a65214fab61..36954252b6b 100644 --- a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb +++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb @@ -20,8 +20,14 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert]) end + let(:mail_body) { "signed hello with Unicode €áø and\r\n newlines\r\n" } + let(:mail) do - ActionMailer::Base.mail(to: 'test@example.com', from: 'info@example.com', body: 'signed hello') + ActionMailer::Base.mail(to: 'test@example.com', + from: 'info@example.com', + content_transfer_encoding: 'quoted-printable', + content_type: 'text/plain; charset=UTF-8', + body: mail_body) end before do @@ -46,9 +52,16 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do ca_cert: root_certificate.cert, signed_data: mail.encoded) + # re-verify signature from a new Mail object content + # See https://gitlab.com/gitlab-org/gitlab/issues/197386 + Gitlab::Email::Smime::Signer.verify_signature( + cert: certificate.cert, + ca_cert: root_certificate.cert, + signed_data: Mail.new(mail).encoded) + # envelope in a Mail object and obtain the body decoded_mail = Mail.new(p7enc.data) - expect(decoded_mail.body.encoded).to eq('signed hello') + expect(decoded_mail.body.decoded.dup.force_encoding(decoded_mail.charset)).to eq(mail_body) end end diff --git a/spec/lib/gitlab/tab_width_spec.rb b/spec/lib/gitlab/tab_width_spec.rb new file mode 100644 index 00000000000..3b5014d27e4 --- /dev/null +++ b/spec/lib/gitlab/tab_width_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::TabWidth, lib: true do + describe '.css_class_for_user' do + it 'returns default CSS class when user is nil' do + css_class = described_class.css_class_for_user(nil) + + expect(css_class).to eq('tab-width-8') + end + + it "returns CSS class for user's tab width", :aggregate_failures do + [1, 6, 12].each do |i| + user = double('user', tab_width: i) + css_class = described_class.css_class_for_user(user) + + expect(css_class).to eq("tab-width-#{i}") + end + end + + it 'raises if tab width is out of valid range', :aggregate_failures do + [0, 13, 'foo', nil].each do |i| + expect do + user = double('user', tab_width: i) + described_class.css_class_for_user(user) + end.to raise_error(ArgumentError) + end + end + end +end diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb index 5c14d57cf18..568699cf3f6 100644 --- a/spec/models/deploy_token_spec.rb +++ b/spec/models/deploy_token_spec.rb @@ -7,6 +7,8 @@ describe DeployToken do it { is_expected.to have_many :project_deploy_tokens } it { is_expected.to have_many(:projects).through(:project_deploy_tokens) } + it { is_expected.to have_many :group_deploy_tokens } + it { is_expected.to have_many(:groups).through(:group_deploy_tokens) } it_behaves_like 'having unique enum values' @@ -17,6 +19,29 @@ describe DeployToken do it { is_expected.to allow_value('GitLab+deploy_token-3.14').for(:username) } it { is_expected.not_to allow_value('<script>').for(:username).with_message(username_format_message) } it { is_expected.not_to allow_value('').for(:username).with_message(username_format_message) } + it { is_expected.to validate_presence_of(:deploy_token_type) } + end + + describe 'deploy_token_type validations' do + context 'when a deploy token is associated to a group' do + it 'does not allow setting a project to it' do + group_token = create(:deploy_token, :group) + group_token.projects << build(:project) + + expect(group_token).not_to be_valid + expect(group_token.errors.full_messages).to include('Deploy token cannot have projects assigned') + end + end + + context 'when a deploy token is associated to a project' do + it 'does not allow setting a group to it' do + project_token = create(:deploy_token) + project_token.groups << build(:group) + + expect(project_token).not_to be_valid + expect(project_token.errors.full_messages).to include('Deploy token cannot have groups assigned') + end + end end describe '#ensure_token' do @@ -125,33 +150,148 @@ describe DeployToken do end end + describe '#holder' do + subject { deploy_token.holder } + + context 'when the token is of project type' do + it 'returns the relevant holder token' do + expect(subject).to eq(deploy_token.project_deploy_tokens.first) + end + end + + context 'when the token is of group type' do + let(:group) { create(:group) } + let(:deploy_token) { create(:deploy_token, :group) } + + it 'returns the relevant holder token' do + expect(subject).to eq(deploy_token.group_deploy_tokens.first) + end + end + end + describe '#has_access_to?' do let(:project) { create(:project) } subject { deploy_token.has_access_to?(project) } - context 'when deploy token is active and related to project' do - let(:deploy_token) { create(:deploy_token, projects: [project]) } + context 'when a project is not passed in' do + let(:project) { nil } - it { is_expected.to be_truthy } + it { is_expected.to be_falsy } end - context 'when deploy token is active but not related to project' do - let(:deploy_token) { create(:deploy_token) } + context 'when a project is passed in' do + context 'when deploy token is active and related to project' do + let(:deploy_token) { create(:deploy_token, projects: [project]) } - it { is_expected.to be_falsy } - end + it { is_expected.to be_truthy } + end - context 'when deploy token is revoked and related to project' do - let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) } + context 'when deploy token is active but not related to project' do + let(:deploy_token) { create(:deploy_token) } - it { is_expected.to be_falsy } - end + it { is_expected.to be_falsy } + end - context 'when deploy token is revoked and not related to the project' do - let(:deploy_token) { create(:deploy_token, :revoked) } + context 'when deploy token is revoked and related to project' do + let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) } - it { is_expected.to be_falsy } + it { is_expected.to be_falsy } + end + + context 'when deploy token is revoked and not related to the project' do + let(:deploy_token) { create(:deploy_token, :revoked) } + + it { is_expected.to be_falsy } + end + + context 'and when the token is of group type' do + let_it_be(:group) { create(:group) } + let(:deploy_token) { create(:deploy_token, :group) } + + before do + deploy_token.groups << group + end + + context 'and the allow_group_deploy_token feature flag is turned off' do + it 'is false' do + stub_feature_flags(allow_group_deploy_token: false) + + is_expected.to be_falsy + end + end + + context 'and the allow_group_deploy_token feature flag is turned on' do + before do + stub_feature_flags(allow_group_deploy_token: true) + end + + context 'and the passed-in project does not belong to any group' do + it { is_expected.to be_falsy } + end + + context 'and the passed-in project belongs to the token group' do + it 'is true' do + group.projects << project + + is_expected.to be_truthy + end + end + + context 'and the passed-in project belongs to a subgroup' do + let(:child_group) { create(:group, parent_id: group.id) } + let(:grandchild_group) { create(:group, parent_id: child_group.id) } + + before do + grandchild_group.projects << project + end + + context 'and the token group is an ancestor (grand-parent) of this group' do + it { is_expected.to be_truthy } + end + + context 'and the token group is not ancestor of this group' do + let(:child2_group) { create(:group, parent_id: group.id) } + + it 'is false' do + deploy_token.groups = [child2_group] + + is_expected.to be_falsey + end + end + end + + context 'and the passed-in project does not belong to the token group' do + it { is_expected.to be_falsy } + end + + context 'and the project belongs to a group that is parent of the token group' do + let(:super_group) { create(:group) } + let(:deploy_token) { create(:deploy_token, :group) } + let(:group) { create(:group, parent_id: super_group.id) } + + it 'is false' do + super_group.projects << project + + is_expected.to be_falsey + end + end + end + end + + context 'and the token is of project type' do + let(:deploy_token) { create(:deploy_token, projects: [project]) } + + context 'and the passed-in project is the same as the token project' do + it { is_expected.to be_truthy } + end + + context 'and the passed-in project is not the same as the token project' do + subject { deploy_token.has_access_to?(create(:project)) } + + it { is_expected.to be_falsey } + end + end end end @@ -183,7 +323,7 @@ describe DeployToken do end end - context 'when passign a value' do + context 'when passing a value' do let(:expires_at) { Date.today + 5.months } let(:deploy_token) { create(:deploy_token, expires_at: expires_at) } diff --git a/spec/models/group_deploy_token_spec.rb b/spec/models/group_deploy_token_spec.rb new file mode 100644 index 00000000000..d38abafa7ed --- /dev/null +++ b/spec/models/group_deploy_token_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GroupDeployToken, type: :model do + let(:group) { create(:group) } + let(:deploy_token) { create(:deploy_token) } + + subject(:group_deploy_token) { create(:group_deploy_token, group: group, deploy_token: deploy_token) } + + it { is_expected.to belong_to :group } + it { is_expected.to belong_to :deploy_token } + + it { is_expected.to validate_presence_of :deploy_token } + it { is_expected.to validate_presence_of :group } + it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:group_id) } +end diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index bb88983e140..7884b87cc26 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -85,4 +85,19 @@ describe UserPreference do expect(user_preference.timezone).to eq(Time.zone.tzinfo.name) end end + + describe '#tab_width' do + it 'is set to 8 by default' do + # Intentionally not using factory here to test the constructor. + pref = UserPreference.new + expect(pref.tab_width).to eq(8) + end + + it do + is_expected.to validate_numericality_of(:tab_width) + .only_integer + .is_greater_than_or_equal_to(1) + .is_less_than_or_equal_to(12) + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 74e38e79616..855b8e3a8a7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -20,6 +20,9 @@ describe User, :do_not_mock_admin_mode do describe 'delegations' do it { is_expected.to delegate_method(:path).to(:namespace).with_prefix } + + it { is_expected.to delegate_method(:tab_width).to(:user_preference) } + it { is_expected.to delegate_method(:tab_width=).to(:user_preference).with_arguments(5) } end describe 'associations' do @@ -4126,4 +4129,41 @@ describe User, :do_not_mock_admin_mode do end end end + + describe 'internal methods' do + let_it_be(:user) { create(:user) } + let!(:ghost) { described_class.ghost } + let!(:alert_bot) { described_class.alert_bot } + let!(:non_internal) { [user] } + let!(:internal) { [ghost, alert_bot] } + + it 'returns non internal users' do + expect(described_class.internal).to eq(internal) + expect(internal.all?(&:internal?)).to eq(true) + end + + it 'returns internal users' do + expect(described_class.non_internal).to eq(non_internal) + expect(non_internal.all?(&:internal?)).to eq(false) + end + + describe '#bot?' do + it 'marks bot users' do + expect(user.bot?).to eq(false) + expect(ghost.bot?).to eq(false) + + expect(alert_bot.bot?).to eq(true) + end + end + end + + describe 'bots & humans' do + it 'returns corresponding users' do + human = create(:user) + bot = create(:user, :bot) + + expect(described_class.humans).to match_array([human]) + expect(described_class.bots).to match_array([bot]) + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 1a4b8315fde..3b08726c75a 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -559,4 +559,18 @@ describe ProjectPolicy do end end end + + context 'alert bot' do + let(:current_user) { User.alert_bot } + + subject { described_class.new(current_user, project) } + + it { is_expected.to be_allowed(:reporter_access) } + + context 'within a private project' do + let(:project) { create(:project, :private) } + + it { is_expected.to be_allowed(:admin_issue) } + end + end end |