diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-21 15:09:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-21 15:09:12 +0000 |
commit | a09c6d7e91de9abab7e2ea8dffce1cbb89bf95d8 (patch) | |
tree | bab46ee809784804b92228a0b860f3837b050755 /spec | |
parent | 32ac796097b0b8fbdbc22157307d1849a3e7b35c (diff) | |
download | gitlab-ce-a09c6d7e91de9abab7e2ea8dffce1cbb89bf95d8.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
10 files changed, 78 insertions, 443 deletions
diff --git a/spec/controllers/admin/broadcast_messages_controller_spec.rb b/spec/controllers/admin/broadcast_messages_controller_spec.rb new file mode 100644 index 00000000000..66a54dc10ac --- /dev/null +++ b/spec/controllers/admin/broadcast_messages_controller_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Admin::BroadcastMessagesController do + before do + sign_in(create(:admin)) + end + + describe 'GET /preview' do + render_views + + it 'renders preview partial' do + get :preview, params: { broadcast_message: { message: "Hello, world!" } } + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to render_template(:_preview) + end + end +end diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index bca1bc4df4d..088c8a7a15a 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -146,12 +146,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do end end - it 'pushes `personal_access_tokens_scoped_to_projects` feature flag to the frontend' do - visit profile_personal_access_tokens_path - - expect(page).to have_pushed_frontend_feature_flags(personalAccessTokensScopedToProjects: true) - end - it "prefills token details" do name = 'My PAT' scopes = 'api,read_user' diff --git a/spec/frontend/access_tokens/components/projects_field_spec.js b/spec/frontend/access_tokens/components/projects_field_spec.js deleted file mode 100644 index 1c4fe7bb168..00000000000 --- a/spec/frontend/access_tokens/components/projects_field_spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import { nextTick } from 'vue'; -import { within, fireEvent } from '@testing-library/dom'; -import { mount } from '@vue/test-utils'; -import ProjectsField from '~/access_tokens/components/projects_field.vue'; -import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; - -describe('ProjectsField', () => { - let wrapper; - - const createComponent = ({ inputAttrsValue = '' } = {}) => { - wrapper = mount(ProjectsField, { - propsData: { - inputAttrs: { - id: 'projects', - name: 'projects', - value: inputAttrsValue, - }, - }, - }); - }; - - const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text); - const queryByText = (text) => within(wrapper.element).queryByText(text); - const findAllProjectsRadio = () => queryByLabelText('All projects'); - const findSelectedProjectsRadio = () => queryByLabelText('Selected projects'); - const findProjectsTokenSelector = () => wrapper.findComponent(ProjectsTokenSelector); - const findHiddenInput = () => wrapper.find('input[type="hidden"]'); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('renders label and sub-label', () => { - createComponent(); - - expect(queryByText('Projects')).not.toBe(null); - expect(queryByText('Set access permissions for this token.')).not.toBe(null); - }); - - describe('when `inputAttrs.value` is empty', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders "All projects" radio as checked', () => { - expect(findAllProjectsRadio().checked).toBe(true); - }); - - it('renders "Selected projects" radio as unchecked', () => { - expect(findSelectedProjectsRadio().checked).toBe(false); - }); - - it('sets `projects-token-selector` `initialProjectIds` prop to an empty array', () => { - expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual([]); - }); - }); - - describe('when `inputAttrs.value` is a comma separated list of project IDs', () => { - beforeEach(() => { - createComponent({ inputAttrsValue: '1,2' }); - }); - - it('renders "All projects" radio as unchecked', () => { - expect(findAllProjectsRadio().checked).toBe(false); - }); - - it('renders "Selected projects" radio as checked', () => { - expect(findSelectedProjectsRadio().checked).toBe(true); - }); - - it('sets `projects-token-selector` `initialProjectIds` prop to an array of project IDs', () => { - expect(findProjectsTokenSelector().props('initialProjectIds')).toEqual(['1', '2']); - }); - }); - - it('renders `projects-token-selector` component', () => { - createComponent(); - - expect(findProjectsTokenSelector().exists()).toBe(true); - }); - - it('renders hidden input with correct `name` and `id` attributes', () => { - createComponent(); - - expect(findHiddenInput().attributes()).toEqual( - expect.objectContaining({ - id: 'projects', - name: 'projects', - }), - ); - }); - - describe('when `projects-token-selector` is focused', () => { - beforeEach(() => { - createComponent(); - - findProjectsTokenSelector().vm.$emit('focus'); - }); - - it('auto selects the "Selected projects" radio', () => { - expect(findSelectedProjectsRadio().checked).toBe(true); - }); - - describe('when `projects-token-selector` is changed', () => { - beforeEach(() => { - findProjectsTokenSelector().vm.$emit('input', [ - { - id: 1, - }, - { - id: 2, - }, - ]); - }); - - it('updates the hidden input value to a comma separated list of project IDs', () => { - expect(findHiddenInput().attributes('value')).toBe('1,2'); - }); - - describe('when radio is changed back to "All projects"', () => { - it('removes the hidden input value', async () => { - fireEvent.change(findAllProjectsRadio()); - await nextTick(); - - expect(findHiddenInput().attributes('value')).toBe(''); - }); - }); - }); - }); -}); diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js deleted file mode 100644 index 40aaf16d41f..00000000000 --- a/spec/frontend/access_tokens/components/projects_token_selector_spec.js +++ /dev/null @@ -1,266 +0,0 @@ -import { - GlAvatar, - GlAvatarLabeled, - GlIntersectionObserver, - GlToken, - GlTokenSelector, - GlLoadingIcon, -} from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import produce from 'immer'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; - -import getProjectsQueryResponse from 'test_fixtures/graphql/projects/access_tokens/get_projects.query.graphql.json'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue'; -import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; - -describe('ProjectsTokenSelector', () => { - const getProjectsQueryResponsePage2 = produce( - getProjectsQueryResponse, - (getProjectsQueryResponseDraft) => { - /* eslint-disable no-param-reassign */ - getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false; - getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null; - getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1); - getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100'; - /* eslint-enable no-param-reassign */ - }, - ); - - const runDebounce = () => jest.runAllTimers(); - - const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects; - const project1 = projects[0]; - const project2 = projects[1]; - - let wrapper; - - let resolveGetProjectsQuery; - let resolveGetInitialProjectsQuery; - const getProjectsQueryRequestHandler = jest.fn( - ({ ids }) => - new Promise((resolve) => { - if (ids) { - resolveGetInitialProjectsQuery = resolve; - } else { - resolveGetProjectsQuery = resolve; - } - }), - ); - - const createComponent = ({ - propsData = {}, - apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]), - resolveQueries = true, - } = {}) => { - Vue.use(VueApollo); - - wrapper = extendedWrapper( - mount(ProjectsTokenSelector, { - apolloProvider, - propsData: { - selectedProjects: [], - initialProjectIds: [], - ...propsData, - }, - stubs: ['gl-intersection-observer'], - }), - ); - - runDebounce(); - - if (resolveQueries) { - resolveGetProjectsQuery(getProjectsQueryResponse); - - return waitForPromises(); - } - - return Promise.resolve(); - }; - - const findTokenSelector = () => wrapper.findComponent(GlTokenSelector); - const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]'); - const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); - - it('renders dropdown items with project avatars', async () => { - await createComponent(); - - wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => { - const project = projects[index]; - - expect(avatarLabeledWrapper.attributes()).toEqual( - expect.objectContaining({ - 'entity-id': `${getIdFromGraphQLId(project.id)}`, - 'entity-name': project.name, - ...(project.avatarUrl && { src: project.avatarUrl }), - }), - ); - - expect(avatarLabeledWrapper.props()).toEqual( - expect.objectContaining({ - label: project.name, - subLabel: project.nameWithNamespace, - }), - ); - }); - }); - - it('renders tokens with project avatars', () => { - createComponent({ - propsData: { - selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }], - }, - }); - - const token = wrapper.findComponent(GlToken); - const avatar = token.findComponent(GlAvatar); - - expect(token.text()).toContain(project2.nameWithNamespace); - expect(avatar.attributes('src')).toBe(project2.avatarUrl); - expect(avatar.props()).toEqual( - expect.objectContaining({ - entityId: getIdFromGraphQLId(project2.id), - entityName: project2.name, - }), - ); - }); - - describe('when `enter` key is pressed', () => { - it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => { - createComponent(); - - const event = { - preventDefault: jest.fn(), - }; - - findTokenSelectorInput().trigger('keydown.enter', event); - - expect(event.preventDefault).toHaveBeenCalled(); - }); - }); - - describe('when text input is typed in', () => { - const searchTerm = 'foo bar'; - - beforeEach(async () => { - await createComponent(); - - await findTokenSelectorInput().setValue(searchTerm); - runDebounce(); - }); - - it('makes GraphQL request with `search` variable set', async () => { - expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ - search: searchTerm, - after: null, - first: 20, - ids: null, - }); - }); - - it('sets loading state while waiting for GraphQL request to resolve', async () => { - expect(findTokenSelector().props('loading')).toBe(true); - - resolveGetProjectsQuery(getProjectsQueryResponse); - await waitForPromises(); - - expect(findTokenSelector().props('loading')).toBe(false); - }); - }); - - describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => { - beforeEach(async () => { - await createComponent(); - - findIntersectionObserver().vm.$emit('appear'); - }); - - it('makes GraphQL request with `after` variable set', async () => { - expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({ - after: pageInfo.endCursor, - first: 20, - search: '', - ids: null, - }); - }); - - it('displays loading icon while waiting for GraphQL request to resolve', async () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - - resolveGetProjectsQuery(getProjectsQueryResponsePage2); - await waitForPromises(); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - }); - }); - - describe('when there is not a next page of projects', () => { - it('does not render `GlIntersectionObserver`', async () => { - createComponent({ resolveQueries: false }); - - resolveGetProjectsQuery(getProjectsQueryResponsePage2); - await waitForPromises(); - - expect(findIntersectionObserver().exists()).toBe(false); - }); - }); - - describe('when `GlTokenSelector` emits `input` event', () => { - it('emits `input` event used by `v-model`', () => { - findTokenSelector().vm.$emit('input', project1); - - expect(wrapper.emitted('input')[0]).toEqual([project1]); - }); - }); - - describe('when `GlTokenSelector` emits `focus` event', () => { - it('emits `focus` event', () => { - const event = { fakeEvent: 'foo' }; - findTokenSelector().vm.$emit('focus', event); - - expect(wrapper.emitted('focus')[0]).toEqual([event]); - }); - }); - - describe('when `initialProjectIds` is an empty array', () => { - it('does not request initial projects', async () => { - await createComponent(); - - expect(getProjectsQueryRequestHandler).toHaveBeenCalledTimes(1); - expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith( - expect.objectContaining({ - ids: null, - }), - ); - }); - }); - - describe('when `initialProjectIds` is an array of project IDs', () => { - it('requests those projects and emits `input` event with result', async () => { - await createComponent({ - propsData: { - initialProjectIds: [getIdFromGraphQLId(project1.id), getIdFromGraphQLId(project2.id)], - }, - }); - - resolveGetInitialProjectsQuery(getProjectsQueryResponse); - await waitForPromises(); - - expect(getProjectsQueryRequestHandler).toHaveBeenCalledWith({ - after: '', - first: null, - search: '', - ids: [project1.id, project2.id], - }); - expect(wrapper.emitted('input')[0][0]).toEqual([ - { ...project1, id: getIdFromGraphQLId(project1.id) }, - { ...project2, id: getIdFromGraphQLId(project2.id) }, - ]); - }); - }); -}); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index b6119f1d167..0c611a4a512 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -8,13 +8,11 @@ import { initAccessTokenTableApp, initExpiresAtField, initNewAccessTokenApp, - initProjectsField, initTokensApp, } from '~/access_tokens'; import * as AccessTokenTableApp from '~/access_tokens/components/access_token_table_app.vue'; -import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; +import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue'; import * as NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue'; -import * as ProjectsField from '~/access_tokens/components/projects_field.vue'; import * as TokensApp from '~/access_tokens/components/tokens_app.vue'; import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants'; import { __, sprintf } from '~/locale'; @@ -115,49 +113,28 @@ describe('access tokens', () => { }); }); - describe.each` - initFunction | mountSelector | fieldName | expectedComponent - ${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField} - ${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField} - `('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => { + describe('initExpiresAtField', () => { describe('when mount element exists', () => { - const FakeComponent = Vue.component('FakeComponent', { - props: ['inputAttrs'], - render: () => null, - }); - - const nameAttribute = `access_tokens[${fieldName}]`; - const idAttribute = `access_tokens_${fieldName}`; + const nameAttribute = 'access_tokens[expires_at]'; + const idAttribute = 'access_tokens_expires_at'; beforeEach(() => { - window.gon = { features: { personalAccessTokensScopedToProjects: true } }; - setHTMLFixture( - `<div class="${mountSelector}"> + `<div class="js-access-tokens-expires-at"> <input - name="${nameAttribute}" - data-js-name="${fieldName}" - id="${idAttribute}" + name="access_tokens[expires_at]" + data-js-name="expiresAt" + id="access_tokens_expires_at" placeholder="Foo bar" value="1,2" /> </div>`, ); - - // Mock component so we don't have to deal with mocking Apollo - // eslint-disable-next-line no-param-reassign - expectedComponent.default = FakeComponent; - }); - - afterEach(() => { - delete window.gon; }); it('mounts component and sets `inputAttrs` prop', async () => { - const vueInstance = await initFunction(); - - wrapper = createWrapper(vueInstance); - const component = wrapper.findComponent(FakeComponent); + wrapper = createWrapper(initExpiresAtField()); + const component = wrapper.findComponent(ExpiresAtField); expect(component.exists()).toBe(true); expect(component.props('inputAttrs')).toEqual({ @@ -171,7 +148,7 @@ describe('access tokens', () => { describe('when mount element does not exist', () => { it('returns `null`', () => { - expect(initFunction()).toBe(null); + expect(initExpiresAtField()).toBe(null); }); }); }); diff --git a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb index 81e2a410962..bcd59c34ea2 100644 --- a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb +++ b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb @@ -430,7 +430,7 @@ RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do it do is_expected - .to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/api/#{project.id}") + .to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/#{project.id}") end end end diff --git a/spec/requests/api/internal/error_tracking_spec.rb b/spec/requests/api/internal/error_tracking_spec.rb index 69eb54d5ed2..4c420eb8505 100644 --- a/spec/requests/api/internal/error_tracking_spec.rb +++ b/spec/requests/api/internal/error_tracking_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe API::Internal::ErrorTracking do let(:secret_token) { Gitlab::CurrentSettings.error_tracking_access_token } let(:headers) do - { ::API::Internal::ErrorTracking::GITLAB_ERROR_TRACKING_TOKEN_HEADER => Base64.encode64(secret_token) } + { ::API::Internal::ErrorTracking::GITLAB_ERROR_TRACKING_TOKEN_HEADER => secret_token } end describe 'GET /internal/error_tracking/allowed' do diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb index c967f9ad1f9..cabd0d6732c 100644 --- a/spec/services/issues/clone_service_spec.rb +++ b/spec/services/issues/clone_service_spec.rb @@ -140,11 +140,11 @@ RSpec.describe Issues::CloneService do context 'issue with award emoji' do let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } - it 'copies the award emoji' do + it 'does not copy the award emoji' do old_issue.reload new_issue = clone_service.execute(old_issue, new_project) - expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name + expect(new_issue.reload.award_emoji).to be_empty end end diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index b17c9ffb4fb..5fee5926823 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -14,16 +14,18 @@ RSpec.describe WorkItems::UpdateService do let(:current_user) { developer } describe '#execute' do - subject(:update_work_item) do + let(:service) do described_class.new( project: project, current_user: current_user, params: opts, spam_params: spam_params, widget_params: widget_params - ).execute(work_item) + ) end + subject(:update_work_item) { service.execute(work_item) } + before do stub_spam_services end @@ -142,6 +144,21 @@ RSpec.describe WorkItems::UpdateService do expect(work_item.description).to eq('changed') end + + context 'when work item validation fails' do + let(:opts) { { title: '' } } + + it 'returns validation errors' do + expect(update_work_item[:message]).to contain_exactly("Title can't be blank") + end + + it 'does not execute after-update widgets', :aggregate_failures do + expect(service).to receive(:update).and_call_original + expect(service).not_to receive(:execute_widgets).with(callback: :update, widget_params: widget_params) + + expect { update_work_item }.not_to change(work_item, :description) + end + end end context 'for the hierarchy widget' do @@ -175,6 +192,22 @@ RSpec.describe WorkItems::UpdateService do end.to not_change(WorkItems::ParentLink, :count).and(not_change(work_item, :title)) end end + + context 'when work item validation fails' do + let(:opts) { { title: '' } } + + it 'returns validation errors' do + expect(update_work_item[:message]).to contain_exactly("Title can't be blank") + end + + it 'does not execute after-update widgets', :aggregate_failures do + expect(service).to receive(:update).and_call_original + expect(service).not_to receive(:execute_widgets).with(callback: :before_update_in_transaction, widget_params: widget_params) + expect(work_item.work_item_children).not_to include(child_work_item) + + update_work_item + end + end end end end diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb index 4f6ff1b8676..4e155d18fc0 100644 --- a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb +++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb @@ -29,6 +29,14 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end end + context 'when invalid params are present' do + let(:params) { { other_parent: parent_work_item } } + + it_behaves_like 'raises a WidgetError' do + let(:message) { 'One or more arguments are invalid: other_parent.' } + end + end + context 'when updating children' do let_it_be(:child_work_item2) { create(:work_item, :task, project: project) } let_it_be(:child_work_item3) { create(:work_item, :task, project: project) } |