diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-01 15:12:10 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-01 15:12:10 +0300 |
commit | 9c5341dd0832c3af377191c461c800e1aa048b10 (patch) | |
tree | e1343570ed06960c320200c8a35f2675a6ec2b48 /spec | |
parent | 46f35a616740504125aaf2c7d20a8bc7ff755ec1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
56 files changed, 653 insertions, 251 deletions
diff --git a/spec/components/docs/01_overview.html.erb b/spec/components/docs/01_overview.html.erb new file mode 100644 index 00000000000..da4178ebcb5 --- /dev/null +++ b/spec/components/docs/01_overview.html.erb @@ -0,0 +1,20 @@ +--- +title: Welcome to our Lookbook 👋 +--- + +<p>With Lookbook we can navigate, inspect and interact with our ViewComponent previews.</p> + +<h2>Usage</h2> + +<ul> + <li>Use the sidebar on the left to navigate our component previews.</li> + <li>Many previews can be interacted with by making changes in the <em>Params</em> tab.</li> + <li>Some previews have additional usage instructions in their <em>Notes</em> tab.</li> +</ul> + +<h2>Learn more</h2> + +<ul> + <li>Learn all about <a href="https://viewcomponent.org/">ViewComponent</a> and <a href="https://github.com/allmarkedup/lookbook">Lookbook</a>.</li> + <li>Have a look at our ViewComponent page in the <a href="https://docs.gitlab.com/ee/development/fe_guide/view_component.html">Frontend development docs</a>.</li> +</ul> diff --git a/spec/components/previews/pajamas/alert_component_preview.rb b/spec/components/previews/pajamas/alert_component_preview.rb new file mode 100644 index 00000000000..9a6b77715f5 --- /dev/null +++ b/spec/components/previews/pajamas/alert_component_preview.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module Pajamas + class AlertComponentPreview < ViewComponent::Preview + # @param body text + # @param dismissible toggle + # @param variant select [info, warning, success, danger, tip] + def default(body: nil, dismissible: true, variant: :info) + render(Pajamas::AlertComponent.new( + title: "Title", + dismissible: dismissible, + variant: variant.to_sym + )) do |c| + if body + c.with_body { body } + end + end + end + end +end diff --git a/spec/components/previews/pajamas/avatar_component_preview.rb b/spec/components/previews/pajamas/avatar_component_preview.rb new file mode 100644 index 00000000000..e5cdde1ccef --- /dev/null +++ b/spec/components/previews/pajamas/avatar_component_preview.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +module Pajamas + class AvatarComponentPreview < ViewComponent::Preview + # Avatar + # ---- + # See its design reference [here](https://design.gitlab.com/components/avatar). + def default + user + end + + # We show user avatars in a circle. + # @param size select [16, 24, 32, 48, 64, 96] + def user(size: 64) + render(Pajamas::AvatarComponent.new(User.first, size: size)) + end + + # @param size select [16, 24, 32, 48, 64, 96] + def project(size: 64) + render(Pajamas::AvatarComponent.new(Project.first, size: size)) + end + + # @param size select [16, 24, 32, 48, 64, 96] + def group(size: 64) + render(Pajamas::AvatarComponent.new(Group.first, size: size)) + end + end +end diff --git a/spec/components/previews/pajamas/banner_component_preview.rb b/spec/components/previews/pajamas/banner_component_preview.rb new file mode 100644 index 00000000000..861e3ff95dc --- /dev/null +++ b/spec/components/previews/pajamas/banner_component_preview.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +module Pajamas + class BannerComponentPreview < ViewComponent::Preview + # Banner + # ---- + # See its design reference [here](https://design.gitlab.com/components/banner). + # + # @param button_text text + # @param button_link text + # @param content textarea + # @param embedded toggle + # @param variant select [introduction, promotion] + def default( + button_text: "Learn more", + button_link: "https://about.gitlab.com/", + content: "Add your message here.", + embedded: false, + variant: :promotion + ) + render(Pajamas::BannerComponent.new( + button_text: button_text, + button_link: button_link, + embedded: embedded, + svg_path: "illustrations/autodevops.svg", + variant: variant + )) do |c| + content_tag :p, content + end + end + + # Use the `primary_action` slot instead of `button_text` and `button_link` if you need something more special, + # like rendering a partial that holds your button. + def with_primary_action_slot + render(Pajamas::BannerComponent.new) do |c| + c.primary_action do + # You could also `render` another partial here. + tag.button "I'm special", class: "btn btn-md btn-confirm gl-button" + end + content_tag :p, "This banner uses the primary_action slot." + end + end + + # Use the `illustration` slot instead of `svg_path` if your illustration is not part or the asset pipeline, + # but for example, an inline SVG via `custom_icon`. + def with_illustration_slot + render(Pajamas::BannerComponent.new) do |c| + c.illustration do + '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>'.html_safe # rubocop:disable Layout/LineLength + end + content_tag :p, "This banner uses the illustration slot." + end + end + end +end diff --git a/spec/components/previews/pajamas/button_component_preview.rb b/spec/components/previews/pajamas/button_component_preview.rb new file mode 100644 index 00000000000..1f61d9cf2bc --- /dev/null +++ b/spec/components/previews/pajamas/button_component_preview.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +module Pajamas + class ButtonComponentPreview < ViewComponent::Preview + # Button + # ---- + # See its design reference [here](https://design.gitlab.com/components/banner). + # + # @param category select [primary, secondary, tertiary] + # @param variant select [default, confirm, danger, dashed, link, reset] + # @param size select [small, medium] + # @param type select [button, reset, submit] + # @param disabled toggle + # @param loading toggle + # @param block toggle + # @param selected toggle + # @param icon text + # @param text text + def default( # rubocop:disable Metrics/ParameterLists + category: :primary, + variant: :default, + size: :medium, + type: :button, + disabled: false, + loading: false, + block: false, + selected: false, + icon: "pencil", + text: "Edit" + ) + render(Pajamas::ButtonComponent.new( + category: category, + variant: variant, + size: size, + type: type, + disabled: disabled, + loading: loading, + block: block, + selected: selected, + icon: icon + )) do + text.presence + end + end + + # The component can also be used to create links that look and feel like buttons. + # Just provide a `href` and optionally a `target` to create an `<a>` tag. + def link + render(Pajamas::ButtonComponent.new( + href: "https://gitlab.com", + target: "_blank" + )) do + "This is a link" + end + end + end +end diff --git a/spec/components/previews/pajamas/card_component_preview.rb b/spec/components/previews/pajamas/card_component_preview.rb new file mode 100644 index 00000000000..61d1f8db9e1 --- /dev/null +++ b/spec/components/previews/pajamas/card_component_preview.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +module Pajamas + class CardComponentPreview < ViewComponent::Preview + # Card + # ---- + # See its design reference [here](https://design.gitlab.com/components/card). + # + # @param header text + # @param body textarea + # @param footer text + def default(header: nil, body: "Every card has a body.", footer: nil) + render(Pajamas::CardComponent.new) do |c| + if header + c.with_header { header } + end + + c.with_body do + content_tag(:p, body) + end + + if footer + c.with_footer { footer } + end + end + end + end +end diff --git a/spec/components/previews/pajamas/spinner_component_preview.rb b/spec/components/previews/pajamas/spinner_component_preview.rb new file mode 100644 index 00000000000..149bfddcfc2 --- /dev/null +++ b/spec/components/previews/pajamas/spinner_component_preview.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +module Pajamas + class SpinnerComponentPreview < ViewComponent::Preview + # Spinner + # ---- + # See its design reference [here](https://design.gitlab.com/components/spinner). + # + # @param inline toggle + # @param label text + # @param size select [[small, sm], [medium, md], [large, lg], [extra large, xl]] + def default(inline: false, label: "Loading", size: :md) + render(Pajamas::SpinnerComponent.new(inline: inline, label: label, size: size)) + end + + # Use a light spinner on dark backgrounds + # + # @display bg_color "#222" + def light + render(Pajamas::SpinnerComponent.new(color: :light)) + end + end +end diff --git a/spec/frontend/__helpers__/mocks/axios_utils.js b/spec/frontend/__helpers__/mocks/axios_utils.js index b1efd29dc8d..60644c84a57 100644 --- a/spec/frontend/__helpers__/mocks/axios_utils.js +++ b/spec/frontend/__helpers__/mocks/axios_utils.js @@ -1,4 +1,6 @@ import EventEmitter from 'events'; +// eslint-disable-next-line no-restricted-syntax +import { setImmediate } from 'timers'; const axios = jest.requireActual('~/lib/utils/axios_utils').default; diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js index ab2637d6024..bdd5a0a9034 100644 --- a/spec/frontend/__helpers__/vuex_action_helper.js +++ b/spec/frontend/__helpers__/vuex_action_helper.js @@ -1,5 +1,7 @@ -/** - * Helper for testing action with expected mutations inspired in +// eslint-disable-next-line no-restricted-syntax +import { setImmediate } from 'timers'; + +/** Helper for testing action with expected mutations inspired in * https://vuex.vuejs.org/en/testing.html * * @param {(Function|Object)} action to be tested, or object of named parameters diff --git a/spec/frontend/__helpers__/wait_for_promises.js b/spec/frontend/__helpers__/wait_for_promises.js index 753c3c5d92b..5a15b8b74b5 100644 --- a/spec/frontend/__helpers__/wait_for_promises.js +++ b/spec/frontend/__helpers__/wait_for_promises.js @@ -1,4 +1,2 @@ -export default () => - new Promise((resolve) => { - requestAnimationFrame(resolve); - }); +// eslint-disable-next-line no-restricted-syntax +export default () => new Promise(jest.requireActual('timers').setImmediate); diff --git a/spec/frontend/__helpers__/web_worker_transformer.js b/spec/frontend/__helpers__/web_worker_transformer.js index 5b2f7d77947..767ab3f5675 100644 --- a/spec/frontend/__helpers__/web_worker_transformer.js +++ b/spec/frontend/__helpers__/web_worker_transformer.js @@ -6,7 +6,7 @@ const babelJestTransformer = require('babel-jest'); // [1]: https://webpack.js.org/loaders/worker-loader/ module.exports = { process: (contentArg, filename, ...args) => { - const { code: content } = babelJestTransformer.process(contentArg, filename, ...args); + const { code: content } = babelJestTransformer.default.process(contentArg, filename, ...args); return `const { FakeWebWorker } = require("helpers/web_worker_fake"); module.exports = class JestTransformedWorker extends FakeWebWorker { diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js index c45cd545155..769d34a4ada 100644 --- a/spec/frontend/boards/project_select_spec.js +++ b/spec/frontend/boards/project_select_spec.js @@ -10,7 +10,6 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import ProjectSelect from '~/boards/components/project_select.vue'; import defaultState from '~/boards/stores/state'; -import waitForPromises from 'helpers/wait_for_promises'; import { mockList, mockActiveGroupProjects } from './mock_data'; @@ -133,7 +132,7 @@ describe('ProjectSelect component', () => { const dropdownToggle = findGlDropdown().find('.dropdown-toggle'); await dropdownToggle.trigger('click'); - await waitForPromises(); + jest.runOnlyPendingTimers(); await nextTick(); const searchInput = findGlDropdown().findComponent(GlFormInput).element; diff --git a/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js b/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js index 08d031a4fa7..2263d2bbeed 100644 --- a/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js +++ b/spec/frontend/captcha/wait_for_captcha_to_be_solved_spec.js @@ -1,3 +1,4 @@ +import { nextTick } from 'vue'; import CaptchaModal from '~/captcha/captcha_modal.vue'; import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved'; @@ -15,7 +16,7 @@ describe('waitForCaptchaToBeSolved', () => { it('opens a modal, resolves with captcha response on success', async () => { CaptchaModal.mounted.mockImplementationOnce(function mounted() { - requestAnimationFrame(() => { + return nextTick().then(() => { this.$emit('receivedCaptchaResponse', response); this.$emit('hidden'); }); @@ -36,7 +37,7 @@ describe('waitForCaptchaToBeSolved', () => { it("opens a modal, rejects with error in case the captcha isn't solved", async () => { CaptchaModal.mounted.mockImplementationOnce(function mounted() { - requestAnimationFrame(() => { + return nextTick().then(() => { this.$emit('receivedCaptchaResponse', null); this.$emit('hidden'); }); diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js index 29884675b24..964dd005a27 100644 --- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js +++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js @@ -150,7 +150,6 @@ describe('InstallAgentModal', () => { }); it("doesn't render agent installation instructions", () => { - expect(findModal().text()).not.toContain(i18n.basicInstallTitle); expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false); expect(findModal().findComponent(GlAlert).exists()).toBe(false); }); diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index 9e4666ffc70..a6bbea648d2 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -85,30 +85,6 @@ describe('AppComponent', () => { await nextTick(); }); - describe('computed', () => { - describe('groups', () => { - it('should return list of groups from store', () => { - jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {}); - - const { groups } = vm; - - expect(vm.store.getGroups).toHaveBeenCalled(); - expect(groups).not.toBeDefined(); - }); - }); - - describe('pageInfo', () => { - it('should return pagination info from store', () => { - jest.spyOn(vm.store, 'getPaginationInfo').mockImplementation(() => {}); - - const { pageInfo } = vm; - - expect(vm.store.getPaginationInfo).toHaveBeenCalled(); - expect(pageInfo).not.toBeDefined(); - }); - }); - }); - describe('methods', () => { describe('fetchGroups', () => { it('should call `getGroups` with all the params provided', () => { diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js index 2ea0c250794..33b33fb62fd 100644 --- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js +++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js @@ -8,12 +8,12 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; const TEST_TABS = [ { title: 'Lorem', - icon: 'angle-up', + icon: 'chevron-lg-up', views: [{ name: 'lorem-1' }, { name: 'lorem-2' }], }, { title: 'Ipsum', - icon: 'angle-down', + icon: 'chevron-lg-down', views: [{ name: 'ipsum-1' }, { name: 'ipsum-2' }], }, ]; diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index 7cf101a5e59..73846aeac96 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -292,16 +292,11 @@ describe('common_utils', () => { const spy = jest.fn(); const debouncedSpy = commonUtils.debounceByAnimationFrame(spy); - return new Promise((resolve) => { - window.requestAnimationFrame(() => { - debouncedSpy(); - debouncedSpy(); - window.requestAnimationFrame(() => { - expect(spy).toHaveBeenCalledTimes(1); - resolve(); - }); - }); - }); + debouncedSpy(); + debouncedSpy(); + jest.runOnlyPendingTimers(); + + expect(spy).toHaveBeenCalledTimes(1); }); }); diff --git a/spec/frontend/lib/utils/rails_ujs_spec.js b/spec/frontend/lib/utils/rails_ujs_spec.js index c10301523c9..da9cc5c6f3c 100644 --- a/spec/frontend/lib/utils/rails_ujs_spec.js +++ b/spec/frontend/lib/utils/rails_ujs_spec.js @@ -18,14 +18,12 @@ function mockXHRResponse({ responseText, responseContentType } = {}) { .mockReturnValue(responseContentType); jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() { - requestAnimationFrame(() => { - Object.defineProperties(this, { - readyState: { value: XMLHttpRequest.DONE }, - status: { value: 200 }, - response: { value: responseText }, - }); - this.onreadystatechange(); + Object.defineProperties(this, { + readyState: { value: XMLHttpRequest.DONE }, + status: { value: 200 }, + response: { value: responseText }, }); + this.onreadystatechange(); }); } diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index d990d5ad22b..cf28ffeabed 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo'; import Vue, { nextTick } from 'vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue'; import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue'; import CiValidate from '~/pipeline_editor/components/validate/ci_validate.vue'; @@ -22,6 +23,7 @@ import { } from '~/pipeline_editor/constants'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql'; +import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql'; import { mockBlobContentQueryResponse, mockCiLintPath, @@ -81,6 +83,15 @@ describe('Pipeline editor tabs component', () => { const createComponentWithApollo = ({ props, provide = {}, mountFn = shallowMount } = {}) => { const handlers = [[getBlobContent, mockBlobContentData]]; mockApollo = createMockApollo(handlers); + mockApollo.clients.defaultClient.cache.writeQuery({ + query: getAppStatus, + data: { + app: { + __typename: 'PipelineEditorApp', + status: EDITOR_APP_STATUS_VALID, + }, + }, + }); createComponent({ props, @@ -203,7 +214,7 @@ describe('Pipeline editor tabs component', () => { }); describe('if badge has been dismissed before', () => { - beforeEach(() => { + it('does not render badge if it has been dismissed before', async () => { localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true'); mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); createComponentWithApollo({ @@ -217,9 +228,9 @@ describe('Pipeline editor tabs component', () => { validateTabIllustrationPath: 'path/to/svg', }, }); - }); - it('does not render badge if it has been dismissed before', () => { + await waitForPromises(); + expect(findBadge().exists()).toBe(false); }); }); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js index 1ff32b03344..e712cdeaea2 100644 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js @@ -1,4 +1,5 @@ import { GlDropdown } from '@gitlab/ui'; +import { nextTick } from 'vue'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -61,11 +62,10 @@ describe('Pipelines stage component', () => { const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]'); - const openStageDropdown = () => { - findDropdownToggle().trigger('click'); - return new Promise((resolve) => { - wrapper.vm.$root.$on('bv::dropdown::show', resolve); - }); + const openStageDropdown = async () => { + await findDropdownToggle().trigger('click'); + await waitForPromises(); + await nextTick(); }; describe('loading state', () => { @@ -77,7 +77,10 @@ describe('Pipelines stage component', () => { await openStageDropdown(); }); - it('displays loading state while jobs are being fetched', () => { + it('displays loading state while jobs are being fetched', async () => { + jest.runOnlyPendingTimers(); + await nextTick(); + expect(findLoadingState().exists()).toBe(true); expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText); }); @@ -98,46 +101,41 @@ describe('Pipelines stage component', () => { expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true); }); - it('should render a dropdown with the status icon', () => { + it('renders a dropdown with the status icon', () => { expect(findDropdown().exists()).toBe(true); expect(findDropdownToggle().exists()).toBe(true); expect(findCiIcon().exists()).toBe(true); }); - it('should render a borderless ci-icon', () => { + it('renders a borderless ci-icon', () => { expect(findCiIcon().exists()).toBe(true); expect(findCiIcon().props('isBorderless')).toBe(true); expect(findCiIcon().classes('borderless')).toBe(true); }); - it('should render a ci-icon with a custom border class', () => { + it('renders a ci-icon with a custom border class', () => { expect(findCiIcon().exists()).toBe(true); expect(findCiIcon().classes('gl-border')).toBe(true); }); }); - describe('when update dropdown is changed', () => { - beforeEach(() => { - createComponent(); - }); - }); - describe('when user opens dropdown and stage request is successful', () => { beforeEach(async () => { mock.onGet(dropdownPath).reply(200, stageReply); createComponent(); await openStageDropdown(); + await jest.runAllTimers(); await axios.waitForAll(); }); - it('should render the received data and emit `clickedDropdown` event', async () => { + it('renders the received data and emit `clickedDropdown` event', async () => { expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); expect(findDropdownMenuTitle().text()).toContain(stageReply.name); expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); }); - it('should refresh when updateDropdown is set to true', async () => { + it('refreshes when updateDropdown is set to true', async () => { expect(mock.history.get).toHaveLength(1); wrapper.setProps({ updateDropdown: true }); @@ -148,15 +146,14 @@ describe('Pipelines stage component', () => { }); describe('when user opens dropdown and stage request fails', () => { - beforeEach(async () => { + it('should close the dropdown', async () => { mock.onGet(dropdownPath).reply(500); createComponent(); await openStageDropdown(); await axios.waitForAll(); - }); + await waitForPromises(); - it('should close the dropdown', () => { expect(findDropdown().classes('show')).toBe(false); }); }); @@ -181,26 +178,29 @@ describe('Pipelines stage component', () => { it('should update the stage to request the new endpoint provided', async () => { await openStageDropdown(); - await axios.waitForAll(); + jest.runOnlyPendingTimers(); + await waitForPromises(); expect(findDropdownMenu().text()).toContain('this is the updated content'); }); }); describe('pipelineActionRequestComplete', () => { - beforeEach(() => { + beforeEach(async () => { mock.onGet(dropdownPath).reply(200, stageReply); mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); createComponent(); + await waitForPromises(); + await nextTick(); }); const clickCiAction = async () => { await openStageDropdown(); - await axios.waitForAll(); + jest.runOnlyPendingTimers(); + await waitForPromises(); - findCiActionBtn().trigger('click'); - await axios.waitForAll(); + await findCiActionBtn().trigger('click'); }; it('closes dropdown when job item action is clicked', async () => { @@ -211,29 +211,30 @@ describe('Pipelines stage component', () => { expect(hidden).toHaveBeenCalledTimes(0); await clickCiAction(); + await waitForPromises(); expect(hidden).toHaveBeenCalledTimes(1); }); it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { await clickCiAction(); + await waitForPromises(); expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); }); }); describe('With merge trains enabled', () => { - beforeEach(async () => { + it('shows a warning on the dropdown', async () => { mock.onGet(dropdownPath).reply(200, stageReply); createComponent({ isMergeTrain: true, }); await openStageDropdown(); - await axios.waitForAll(); - }); + jest.runOnlyPendingTimers(); + await waitForPromises(); - it('shows a warning on the dropdown', () => { const warning = findMergeTrainWarning(); expect(warning.text()).toBe('Merge train pipeline jobs can not be retried'); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index ad6d650670a..0bed24e588e 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -45,6 +45,7 @@ describe('Pipelines', () => { ciLintPath: '/ci/lint', resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`, newPipelinePath: `${mockProjectPath}/pipelines/new`, + ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`, }; @@ -654,7 +655,12 @@ describe('Pipelines', () => { // Mock init a polling cycle wrapper.vm.poll.options.notificationCallback(true); - findStagesDropdownToggle().trigger('click'); + await findStagesDropdownToggle().trigger('click'); + jest.runOnlyPendingTimers(); + + // cancelMock is getting overwritten in pipelines_service.js#L29 + // so we have to spy on it again here + cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); await waitForPromises(); @@ -664,7 +670,8 @@ describe('Pipelines', () => { }); it('stops polling & restarts polling', async () => { - findStagesDropdownToggle().trigger('click'); + await findStagesDropdownToggle().trigger('click'); + jest.runOnlyPendingTimers(); await waitForPromises(); expect(cancelMock).not.toHaveBeenCalled(); diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js index f820951cffc..509681c5a77 100644 --- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -14,6 +14,7 @@ import RunnerPauseButton from '~/runner/components/runner_pause_button.vue'; import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue'; import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; import RunnersJobs from '~/runner/components/runner_jobs.vue'; + import runnerQuery from '~/runner/graphql/show/runner.query.graphql'; import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue'; import { captureException } from '~/runner/sentry_utils'; @@ -182,17 +183,19 @@ describe('AdminRunnerShowApp', () => { }); describe('When loading', () => { - beforeEach(() => { + it('does not show runner details', () => { mockRunnerQueryResult(); createComponent(); - }); - it('does not show runner details', () => { expect(findRunnerDetails().exists()).toBe(false); }); it('does not show runner jobs', () => { + mockRunnerQueryResult(); + + createComponent(); + expect(findRunnersJobs().exists()).toBe(false); }); }); diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index fac99ed395a..bff1cb1164d 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -50,6 +50,7 @@ import { allRunnersDataPaginated, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyPageInfo, emptyStateSvgPath, emptyStateFilteredSvgPath, } from '../mock_data'; @@ -380,13 +381,20 @@ describe('AdminRunnersApp', () => { beforeEach(async () => { mockRunnersHandler.mockResolvedValue({ data: { - runners: { nodes: [] }, + runners: { + nodes: [], + pageInfo: emptyPageInfo, + }, }, }); await createComponent(); }); + it('shows no errors', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); + it('shows an empty state', () => { expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false); }); diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/runner/components/runner_assigned_item_spec.js index 1ff6983fbe7..cc09046c000 100644 --- a/spec/frontend/runner/components/runner_assigned_item_spec.js +++ b/spec/frontend/runner/components/runner_assigned_item_spec.js @@ -1,10 +1,12 @@ -import { GlAvatar } from '@gitlab/ui'; +import { GlAvatar, GlBadge } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue'; import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; const mockHref = '/group/project'; const mockName = 'Project'; +const mockDescription = 'Project description'; const mockFullName = 'Group / Project'; const mockAvatarUrl = '/avatar.png'; @@ -12,6 +14,7 @@ describe('RunnerAssignedItem', () => { let wrapper; const findAvatar = () => wrapper.findByTestId('item-avatar'); + const findBadge = () => wrapper.findComponent(GlBadge); const createComponent = ({ props = {} } = {}) => { wrapper = shallowMountExtended(RunnerAssignedItem, { @@ -20,6 +23,7 @@ describe('RunnerAssignedItem', () => { name: mockName, fullName: mockFullName, avatarUrl: mockAvatarUrl, + description: mockDescription, ...props, }, }); @@ -51,4 +55,14 @@ describe('RunnerAssignedItem', () => { expect(groupFullName.attributes('href')).toBe(mockHref); }); + + it('Shows description', () => { + expect(wrapper.text()).toContain(mockDescription); + }); + + it('Shows owner badge', () => { + createComponent({ props: { isOwner: true } }); + + expect(findBadge().text()).toBe(s__('Runner|Owner')); + }); }); diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js index 6932b3b5197..c988fb8477d 100644 --- a/spec/frontend/runner/components/runner_projects_spec.js +++ b/spec/frontend/runner/components/runner_projects_spec.js @@ -95,6 +95,7 @@ describe('RunnerProjects', () => { name, fullName: nameWithNamespace, avatarUrl, + isOwner: true, // first project is always owner }); }); diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js index d80a15f1fc3..cee1d436942 100644 --- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js +++ b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js @@ -178,13 +178,10 @@ describe('GroupRunnerShowApp', () => { }); describe('When loading', () => { - beforeEach(() => { + it('does not show runner details', () => { mockRunnerQueryResult(); createComponent(); - }); - - it('does not show runner details', () => { expect(findRunnerDetails().exists()).toBe(false); }); }); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 2aa631f270f..99b6273627c 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -47,6 +47,7 @@ import { groupRunnersCountData, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyPageInfo, emptyStateSvgPath, emptyStateFilteredSvgPath, } from '../mock_data'; @@ -331,13 +332,20 @@ describe('GroupRunnersApp', () => { data: { group: { id: '1', - runners: { nodes: [] }, + runners: { + edges: [], + pageInfo: emptyPageInfo, + }, }, }, }); await createComponent(); }); + it('shows no errors', () => { + expect(createAlert).not.toHaveBeenCalled(); + }); + it('shows an empty state', async () => { expect(findRunnerListEmptyState().exists()).toBe(true); }); diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index e5472ace817..e73d888bd9f 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -19,6 +19,14 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runne import { RUNNER_PAGE_SIZE } from '~/runner/constants'; +const emptyPageInfo = { + __typename: 'PageInfo', + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', +}; + // Other mock data // Mock searches and their corresponding urls @@ -233,6 +241,7 @@ export { groupRunnersData, groupRunnersDataPaginated, groupRunnersCountData, + emptyPageInfo, runnerData, runnerWithGroupData, runnerProjectsData, diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js index 8d8c10d10f1..83764cb6739 100644 --- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js +++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js @@ -1,4 +1,5 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { nextTick } from 'vue'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import EscalationStatus from '~/sidebar/components/incidents/escalation_status.vue'; @@ -61,6 +62,8 @@ describe('EscalationStatus', () => { createComponent(); // Open dropdown await toggleDropdown(); + jest.runOnlyPendingTimers(); + await nextTick(); expect(findDropdownMenu().classes('show')).toBe(true); @@ -74,6 +77,8 @@ describe('EscalationStatus', () => { createComponent({ preventDropdownClose: true }); // Open dropdown await toggleDropdown(); + jest.runOnlyPendingTimers(); + await nextTick(); expect(findDropdownMenu().classes('show')).toBe(true); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index b4626625f31..bcd7c651fa7 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -1,4 +1,6 @@ /* Setup for unit test environment */ +// eslint-disable-next-line no-restricted-syntax +import { setImmediate } from 'timers'; import 'helpers/shared_test_setup'; import { initializeTestTimeout } from 'helpers/timeout'; diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js index 299949a4baa..547034bad45 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -204,7 +204,7 @@ describe('WorkItemAssignees component', () => { expect(findTokenSelector().props('dropdownItems')).toHaveLength(2); }); - it('should search for users with correct key after text input', async () => { + it('searches for users with correct key after text input', async () => { const searchKey = 'Hello'; findTokenSelector().vm.$emit('focus'); diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index d29447ee376..becfdced5fb 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -54,16 +54,16 @@ RSpec.describe ::Gitlab::BareRepositoryImport::Repository do end context 'hashed storage' do - let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' } let(:hashed_path) { "@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" } let(:root_path) { TestEnv.repos_path } let(:repo_path) { File.join(root_path, "#{hashed_path}.git") } let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") } let(:raw_repository) { Gitlab::Git::Repository.new('default', "#{hashed_path}.git", nil, nil) } + let(:full_path) { 'to/repo' } before do raw_repository.create_repository - raw_repository.set_full_path(full_path: 'to/repo') + raw_repository.set_full_path(full_path: full_path) if full_path end after do @@ -95,16 +95,17 @@ RSpec.describe ::Gitlab::BareRepositoryImport::Repository do expect(subject).not_to be_processable end - it 'returns false when group and project name are missing' do - repository = Rugged::Repository.new(repo_path) - repository.config.delete('gitlab.fullpath') - - expect(subject).not_to be_processable - end - it 'returns true when group path and project name are present' do expect(subject).to be_processable end + + context 'group and project name are missing' do + let(:full_path) { nil } + + it 'returns false' do + expect(subject).not_to be_processable + end + end end describe '#project_full_path' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e20d5b928c4..0b5c066430d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2017,17 +2017,14 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do describe '#set_full_path' do before do - repository_rugged.config["gitlab.fullpath"] = repository_path + repository.set_full_path(full_path: repository_path) end context 'is given a path' do it 'writes it to disk' do repository.set_full_path(full_path: "not-the/real-path.git") - config = File.read(File.join(repository_path, "config")) - - expect(config).to include("[gitlab]") - expect(config).to include("fullpath = not-the/real-path.git") + expect(repository.full_path).to eq('not-the/real-path.git') end end @@ -2035,15 +2032,12 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it 'does not write it to disk' do repository.set_full_path(full_path: "") - config = File.read(File.join(repository_path, "config")) - - expect(config).to include("[gitlab]") - expect(config).to include("fullpath = #{repository_path}") + expect(repository.full_path).to eq(repository_path) end end context 'repository does not exist' do - it 'raises NoRepository and does not call Gitaly WriteConfig' do + it 'raises NoRepository and does not call SetFullPath' do repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project') expect(repository.gitaly_repository_client).not_to receive(:set_full_path) @@ -2055,6 +2049,18 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end + describe '#full_path' do + let(:full_path) { 'some/path' } + + before do + repository.set_full_path(full_path: full_path) + end + + it 'returns the full path' do + expect(repository.full_path).to eq(full_path) + end + end + describe '#merge_to_ref' do let(:repository) { mutable_repository } let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 31a5bc737ba..8577cad1011 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -153,11 +153,6 @@ RSpec.describe Gitlab::GitAccess, :aggregate_failures do end it 'logs' do - allow(Gitlab::AppJsonLogger).to receive(:info).with( - hash_including( - "class" => "AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker" - ) - ) expect(Gitlab::AppJsonLogger).to receive(:info).with( message: 'Actor was :ci', project_id: project.id @@ -751,11 +746,6 @@ RSpec.describe Gitlab::GitAccess, :aggregate_failures do it 'logs' do expect(Gitlab::AppJsonLogger).to receive(:info).with( - hash_including( - "class" => "AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker" - ) - ).once - expect(Gitlab::AppJsonLogger).to receive(:info).with( message: 'Actor was :ci', project_id: project.id ).once diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 39de9a65390..2baa1573676 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -351,4 +351,16 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do client.set_full_path(path) end end + + describe '#full_path' do + let(:path) { 'repo/path' } + + it 'sends a full_path message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:full_path) + .and_return(double(path: path)) + + expect(client.full_path).to eq(path) + end + end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index c076346c619..c6266f15340 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -166,11 +166,12 @@ RSpec.describe GroupMember do let_it_be(:project_c) { create(:project, group: group) } let_it_be(:user) { create(:user) } - shared_examples_for 'calls UserProjectAccessChangedService to recalculate authorizations' do - it 'calls UserProjectAccessChangedService to recalculate authorizations' do - expect_next_instance_of(UserProjectAccessChangedService, user.id) do |service| - expect(service).to receive(:execute).with(blocking: blocking) - end + shared_examples_for 'calls AuthorizedProjectsWorker inline to recalculate authorizations' do + # this is inline with the overridden behaviour in stubbed_member.rb + it 'calls AuthorizedProjectsWorker inline to recalculate authorizations' do + worker_instance = AuthorizedProjectsWorker.new + expect(AuthorizedProjectsWorker).to receive(:new).and_return(worker_instance) + expect(worker_instance).to receive(:perform).with(user.id) action end @@ -178,15 +179,14 @@ RSpec.describe GroupMember do context 'on create' do let(:action) { group.add_member(user, Gitlab::Access::GUEST) } - let(:blocking) { true } - it 'changes access level', :sidekiq_inline do + it 'changes access level' do expect { action }.to change { user.can?(:guest_access, project_a) }.from(false).to(true) .and change { user.can?(:guest_access, project_b) }.from(false).to(true) .and change { user.can?(:guest_access, project_c) }.from(false).to(true) end - it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations' + it_behaves_like 'calls AuthorizedProjectsWorker inline to recalculate authorizations' end context 'on update' do @@ -195,15 +195,14 @@ RSpec.describe GroupMember do end let(:action) { group.members.find_by(user: user).update!(access_level: Gitlab::Access::DEVELOPER) } - let(:blocking) { true } - it 'changes access level', :sidekiq_inline do + it 'changes access level' do expect { action }.to change { user.can?(:developer_access, project_a) }.from(false).to(true) .and change { user.can?(:developer_access, project_b) }.from(false).to(true) .and change { user.can?(:developer_access, project_c) }.from(false).to(true) end - it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations' + it_behaves_like 'calls AuthorizedProjectsWorker inline to recalculate authorizations' end context 'on destroy' do @@ -212,7 +211,6 @@ RSpec.describe GroupMember do end let(:action) { group.members.find_by(user: user).destroy! } - let(:blocking) { false } it 'changes access level', :sidekiq_inline do expect { action }.to change { user.can?(:guest_access, project_a) }.from(true).to(false) @@ -220,7 +218,11 @@ RSpec.describe GroupMember do .and change { user.can?(:guest_access, project_c) }.from(true).to(false) end - it_behaves_like 'calls UserProjectAccessChangedService to recalculate authorizations' + it 'schedules an AuthorizedProjectsWorker job to recalculate authorizations' do + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async).with([[user.id]]) + + action + end end end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 39d9d25a98c..99fc5dc14df 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -213,10 +213,11 @@ RSpec.describe ProjectMember do let_it_be(:user) { create(:user) } shared_examples_for 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker inline to recalculate authorizations' do - it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker' do - expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).to receive(:bulk_perform_and_wait).with( - [[project.id, user.id]] - ) + # this is inline with the overridden behaviour in stubbed_member.rb + it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker inline' do + worker_instance = AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.new + expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).to receive(:new).and_return(worker_instance) + expect(worker_instance).to receive(:perform).with(project.id, user.id) action end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 106e6a9faf0..695cd98fa29 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe Namespace do include ProjectForksHelper - include GitHelpers include ReloadHelpers let_it_be(:group_sti_name) { Group.sti_name } @@ -1076,9 +1075,9 @@ RSpec.describe Namespace do it 'updates project full path in .git/config' do parent.update!(path: 'mygroup_new') - expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" - expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" - expect(project_rugged(legacy_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + expect(project_in_parent_group.reload.repository.full_path).to eq "mygroup_new/#{project_in_parent_group.path}" + expect(hashed_project_in_subgroup.reload.repository.full_path).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" + expect(legacy_project_in_subgroup.reload.repository.full_path).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" end it 'updates the project storage location' do @@ -1092,14 +1091,6 @@ RSpec.describe Namespace do expect(repository_hashed_project_in_subgroup.reload.disk_path).to eq hashed_project_in_subgroup.disk_path expect(repository_legacy_project_in_subgroup.reload.disk_path).to eq "mygroup_moved/mysubgroup/#{legacy_project_in_subgroup.path}" end - - def project_rugged(project) - # Routes are loaded when creating the projects, so we need to manually - # reload them for the below code to be aware of the above UPDATE. - project.route.reload - - rugged_repo(project.repository) - end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cbaf3d0b3d5..b434b2c0332 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe Project, factory_default: :keep do include ProjectForksHelper - include GitHelpers include ExternalAuthorizationServiceHelpers include ReloadHelpers include StubGitlabCalls @@ -5741,16 +5740,18 @@ RSpec.describe Project, factory_default: :keep do describe '#set_full_path' do let_it_be(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + it 'writes full path in .git/config when key is missing' do project.set_full_path - expect(rugged_config['gitlab.fullpath']).to eq project.full_path + expect(repository.full_path).to eq project.full_path end it 'updates full path in .git/config when key is present' do project.set_full_path(gl_full_path: 'old/path') - expect { project.set_full_path }.to change { rugged_config['gitlab.fullpath'] }.from('old/path').to(project.full_path) + expect { project.set_full_path }.to change { repository.full_path }.from('old/path').to(project.full_path) end it 'does not raise an error with an empty repository' do @@ -8436,10 +8437,6 @@ RSpec.describe Project, factory_default: :keep do export_job.finish end - def rugged_config - rugged_repo(project.repository).config - end - def create_pipeline(project, status = 'success') create(:ci_pipeline, project: project, sha: project.commit.sha, diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 51351c9fdd1..62f50dc2874 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe RemoteMirror, :mailer do - include GitHelpers - before do stub_feature_flags(remote_mirror_no_delay: false) end @@ -96,16 +94,6 @@ RSpec.describe RemoteMirror, :mailer do expect(mirror.url).to eq('http://foo:bar@test.com') expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' }) end - - it 'does not update the repository config if credentials changed' do - mirror = create_mirror(url: 'http://foo:bar@test.com') - repo = mirror.project.repository - old_config = rugged_repo(repo).config - - mirror.update_attribute(:url, 'http://foo:baz@test.com') - - expect(rugged_repo(repo).config.to_hash).to eq(old_config.to_hash) - end end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 3ef859376a4..c513baea517 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe GroupPolicy do include_context 'GroupPolicy context' - using RSpec::Parameterized::TableSyntax context 'public group with no user' do let(:group) { create(:group, :public, :crm_enabled) } @@ -1230,30 +1229,4 @@ RSpec.describe GroupPolicy do it { is_expected.to be_disallowed(:admin_crm_contact) } it { is_expected.to be_disallowed(:admin_crm_organization) } end - - describe 'maintain_namespace' do - context 'with non-admin roles' do - where(:role, :allowed) do - :guest | false - :reporter | false - :developer | false - :maintainer | true - :owner | true - end - - with_them do - let(:current_user) { public_send(role) } - - it do - expect(subject.allowed?(:maintain_namespace)).to eq allowed - end - end - end - - context 'as an admin', :enable_admin_mode do - let(:current_user) { admin } - - it { is_expected.to be_allowed(:maintain_namespace) } - end - end end diff --git a/spec/policies/namespaces/user_namespace_policy_spec.rb b/spec/policies/namespaces/user_namespace_policy_spec.rb index e8a3c9b828d..22c3f6a6d67 100644 --- a/spec/policies/namespaces/user_namespace_policy_spec.rb +++ b/spec/policies/namespaces/user_namespace_policy_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Namespaces::UserNamespacePolicy do let_it_be(:admin) { create(:admin) } let_it_be(:namespace) { create(:user_namespace, owner: owner) } - let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package, :maintain_namespace] } + let(:owner_permissions) { [:owner_access, :create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects, :admin_package] } subject { described_class.new(current_user, namespace) } diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index b6cb790bb71..260f7cbc226 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -262,4 +262,54 @@ RSpec.describe API::API do end end end + + describe 'content security policy header' do + let_it_be(:user) { create(:user) } + + let(:csp) { nil } + let(:report_only) { false } + + subject { get api("/users/#{user.id}", user) } + + before do + allow(Rails.application.config).to receive(:content_security_policy).and_return(csp) + allow(Rails.application.config).to receive(:content_security_policy_report_only).and_return(report_only) + end + + context 'when CSP is not configured globally' do + it 'does not set the CSP header' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Security-Policy']).to be_nil + end + end + + context 'when CSP is configured globally' do + let(:csp) do + ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src :self + end + end + + it 'sets a stricter CSP header' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Security-Policy']).to eq("default-src 'none'") + end + + context 'when report_only is true' do + let(:report_only) { true } + + it 'does not set any CSP header' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Security-Policy']).to be_nil + expect(response.headers['Content-Security-Policy-Report-Only']).to be_nil + end + end + end + end end diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index cb351635081..a795b49c44e 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -447,7 +447,7 @@ RSpec.describe API::Invitations do emails = 'email3@example.com,email4@example.com,email5@example.com,email6@example.com,email7@example.com' - unresolved_n_plus_ones = 32 # currently there are 8 queries added per email + unresolved_n_plus_ones = 36 # currently there are 9 queries added per email expect do post invitations_url(group, maintainer), params: { email: emails, access_level: Member::DEVELOPER } diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb index 4130fbd44fa..fced7195046 100644 --- a/spec/services/members/groups/creator_service_spec.rb +++ b/spec/services/members/groups/creator_service_spec.rb @@ -27,7 +27,10 @@ RSpec.describe Members::Groups::CreatorService do context 'authorized projects update' do it 'schedules a single project authorization update job when called multiple times' do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once + # this is inline with the overridden behaviour in stubbed_member.rb + worker_instance = AuthorizedProjectsWorker.new + expect(AuthorizedProjectsWorker).to receive(:new).once.and_return(worker_instance) + expect(worker_instance).to receive(:perform).with(user.id) 1.upto(3) do described_class.add_member(source, user, :maintainer) diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index 9dc15131bc5..edf4bbe0f7f 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' RSpec.describe Projects::AfterRenameService do - let(:rugged_config) { rugged_repo(project.repository).config } let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::Hashed.new(project) } let!(:path_before_rename) { project.path } @@ -71,10 +70,10 @@ RSpec.describe Projects::AfterRenameService do end end - it 'updates project full path in .git/config' do + it 'updates project full path in gitaly' do service_execute - expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.full_path).to eq(project.full_path) end it 'updates storage location' do @@ -173,10 +172,10 @@ RSpec.describe Projects::AfterRenameService do end end - it 'updates project full path in .git/config' do + it 'updates project full path in gitaly' do service_execute - expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.full_path).to eq(project.full_path) end it 'updates storage location' do diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 59dee209ff9..9486f69fa01 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe Projects::CreateService, '#execute' do include ExternalAuthorizationServiceHelpers - include GitHelpers let(:user) { create :user } let(:project_name) { 'GitLab' } @@ -769,11 +768,10 @@ RSpec.describe Projects::CreateService, '#execute' do create_project(user, opts) end - it 'writes project full path to .git/config' do + it 'writes project full path to gitaly' do project = create_project(user, opts) - rugged = rugged_repo(project.repository) - expect(rugged.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end it 'triggers PostCreationWorker' do diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index d0064873972..65da1976dc2 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -68,12 +68,10 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do service.execute end - it 'writes project full path to .git/config' do + it 'writes project full path to gitaly' do service.execute - rugged_config = rugged_repo(project.repository).config['gitlab.fullpath'] - - expect(rugged_config).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end end diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb index 23e776b72bc..385c03e6308 100644 --- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis_shared_state do - include GitHelpers - let(:gitlab_shell) { Gitlab::Shell.new } let(:project) { create(:project, :repository, :wiki_repo, :design_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) } let(:legacy_storage) { Storage::LegacyProject.new(project) } @@ -68,12 +66,10 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab service.execute end - it 'writes project full path to .git/config' do + it 'writes project full path to gitaly' do service.execute - rugged_config = rugged_repo(project.repository).config['gitlab.fullpath'] - - expect(rugged_config).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 1edc12bbe37..8f505c31c5a 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Projects::TransferService do - include GitHelpers - let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') } @@ -202,10 +200,10 @@ RSpec.describe Projects::TransferService do expect(project.disk_path).to start_with(group.path) end - it 'updates project full path in .git/config' do + it 'updates project full path in gitaly' do execute_transfer - expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + expect(project.repository.full_path).to eq "#{group.full_path}/#{project.path}" end it 'updates storage location' do @@ -296,10 +294,10 @@ RSpec.describe Projects::TransferService do expect(original_path).to eq current_path end - it 'rolls back project full path in .git/config' do + it 'rolls back project full path in gitaly' do attempt_project_transfer - expect(rugged_config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end it "doesn't send move notifications" do @@ -770,10 +768,6 @@ RSpec.describe Projects::TransferService do end end - def rugged_config - rugged_repo(project.repository).config - end - def project_namespace_in_sync(group) project.reload expect(project.namespace).to eq(group) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 47cd78873f8..921d6503099 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -208,6 +208,7 @@ RSpec.configure do |config| include StubFeatureFlags include StubSnowplow + include StubMember if ENV['CI'] || ENV['RETRIES'] # This includes the first try, i.e. tests will be run 4 times before failing. diff --git a/spec/support/helpers/stub_member.rb b/spec/support/helpers/stub_member.rb new file mode 100644 index 00000000000..bcd0b675041 --- /dev/null +++ b/spec/support/helpers/stub_member.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module StubMember + def self.included(base) + Member.prepend(StubbedMember::Member) + ProjectMember.prepend(StubbedMember::ProjectMember) + end +end diff --git a/spec/support/helpers/stubbed_member.rb b/spec/support/helpers/stubbed_member.rb new file mode 100644 index 00000000000..27420c9b709 --- /dev/null +++ b/spec/support/helpers/stubbed_member.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Extend the ProjectMember & GroupMember class with the ability to +# to run project_authorizations refresh jobs inline. + +# This is needed so that calls like `group.add_member(user, access_level)` or `create(:project_member)` +# in the specs can be run without including `:sidekiq_inline` trait. +module StubbedMember + extend ActiveSupport::Concern + + module Member + private + + def refresh_member_authorized_projects(blocking:) + return super unless blocking + + AuthorizedProjectsWorker.new.perform(user_id) + end + end + + module ProjectMember + private + + def blocking_project_authorizations_refresh + AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.new.perform(project.id, user.id) + end + end +end diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb index 14f6a42d7f4..4ecb924b3ed 100644 --- a/spec/support/matchers/event_store.rb +++ b/spec/support/matchers/event_store.rb @@ -23,8 +23,8 @@ RSpec::Matchers.define :publish_event do |expected_event_class| def match_data?(actual, expected) values_match?(actual.keys, expected.keys) && - actual.keys.each do |key| - values_match?(actual[key], expected[key]) + actual.keys.all? do |key| + values_match?(expected[key], actual[key]) end end diff --git a/spec/tooling/danger/customer_success_spec.rb b/spec/tooling/danger/customer_success_spec.rb new file mode 100644 index 00000000000..798905212f1 --- /dev/null +++ b/spec/tooling/danger/customer_success_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'rspec-parameterized' +require 'gitlab-dangerfiles' +require 'gitlab/dangerfiles/spec_helper' +require_relative '../../../tooling/danger/customer_success' + +RSpec.describe Tooling::Danger::CustomerSuccess do + include_context "with dangerfile" + + let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } + let(:customer_success) { fake_danger.new(helper: fake_helper) } + + describe 'customer success danger' do + using RSpec::Parameterized::TableSyntax + + where do + { + 'with data category changes to Ops and no Customer Success::Impact Check label' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb), + changed_lines: ['-data_category: cat1', '+data_category: operational'], + customer_labeled: false, + impacted: true, + impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml) + }, + 'with data category changes and Customer Success::Impact Check label' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml), + changed_lines: ['-data_category: cat1', '+data_category: operational'], + customer_labeled: true, + impacted: false, + impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml) + }, + 'with metric file changes and no data category changes' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml), + changed_lines: ['-product_stage: growth'], + customer_labeled: false, + impacted: false, + impacted_files: [] + }, + 'with data category changes from Ops' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb), + changed_lines: ['-data_category: operational', '+data_category: cat2'], + customer_labeled: false, + impacted: true, + impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml) + }, + 'with data category removed' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb), + changed_lines: ['-data_category: operational'], + customer_labeled: false, + impacted: true, + impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml) + }, + 'with data category added' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb), + changed_lines: ['+data_category: operational'], + customer_labeled: false, + impacted: true, + impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml) + }, + 'with data category in uppercase' => { + modified_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml app/models/user.rb), + changed_lines: ['+data_category: Operational'], + customer_labeled: false, + impacted: true, + impacted_files: %w(config/metrics/20210216182127_user_secret_detection_jobs.yml) + } + } + end + + with_them do + before do + allow(fake_helper).to receive(:modified_files).and_return(modified_files) + allow(fake_helper).to receive(:changed_lines).and_return(changed_lines) + allow(fake_helper).to receive(:has_scoped_label_with_scope?).and_return(customer_labeled) + allow(fake_helper).to receive(:markdown_list).with(impacted_files) + .and_return(impacted_files.map { |item| "* `#{item}`" }.join("\n")) + end + + it 'generates correct message' do + expect(customer_success.build_message).to match_expected_message + end + end + end + + def match_expected_message + return be_nil unless impacted + + start_with(described_class::CHANGED_SCHEMA_MESSAGE).and(include(*impacted_files)) + end +end diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb index f6d4cc4679d..bf156c3b8cb 100644 --- a/spec/workers/concerns/waitable_worker_spec.rb +++ b/spec/workers/concerns/waitable_worker_spec.rb @@ -30,19 +30,33 @@ RSpec.describe WaitableWorker do describe '.bulk_perform_and_wait' do context '1 job' do - it 'inlines the job' do - args_list = [[1]] - expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original - expect(Gitlab::AppJsonLogger).to( - receive(:info).with(a_hash_including('message' => 'running inline', - 'class' => 'Gitlab::Foo::Bar::DummyWorker', - 'job_status' => 'running', - 'queue' => 'foo_bar_dummy')) - .once) - - worker.bulk_perform_and_wait(args_list) - - expect(worker.counter).to eq(1) + it 'runs the jobs asynchronously' do + arguments = [[1]] + + expect(worker).to receive(:bulk_perform_async).with(arguments) + + worker.bulk_perform_and_wait(arguments) + end + + context 'when the feature flag `always_async_project_authorizations_refresh` is turned off' do + before do + stub_feature_flags(always_async_project_authorizations_refresh: false) + end + + it 'inlines the job' do + args_list = [[1]] + expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original + expect(Gitlab::AppJsonLogger).to( + receive(:info).with(a_hash_including('message' => 'running inline', + 'class' => 'Gitlab::Foo::Bar::DummyWorker', + 'job_status' => 'running', + 'queue' => 'foo_bar_dummy')) + .once) + + worker.bulk_perform_and_wait(args_list) + + expect(worker.counter).to eq(1) + end end end |