diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-09 18:10:05 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-09 18:10:05 +0300 |
commit | c0f42c6d662b776777afbf79ba72d8e833b8de48 (patch) | |
tree | d94d38bccd5297f59522090fd3c814d9264a1cc9 /spec | |
parent | f4d6d3ec77286fa64810bd6a25c58671e0deedaf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
24 files changed, 836 insertions, 186 deletions
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index c418ceeae61..bea0aa10c64 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -432,7 +432,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do it 'user cannot remove source branch', :sidekiq_might_not_need_inline do expect(page).not_to have_field('remove-source-branch-input') - expect(page).to have_content('Deletes source branch') + expect(page).to have_content('The source branch will be deleted') end end diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb index 0d8c75a099d..dfe3e76f172 100644 --- a/spec/features/nav/top_nav_responsive_spec.rb +++ b/spec/features/nav/top_nav_responsive_spec.rb @@ -6,7 +6,6 @@ RSpec.describe 'top nav responsive', :js do include MobileHelpers let_it_be(:user) { create(:user) } - let_it_be(:responsive_menu_text) { 'Placeholder for responsive top nav' } before do stub_feature_flags(combined_menu: true) @@ -20,7 +19,9 @@ RSpec.describe 'top nav responsive', :js do context 'before opened' do it 'has page content and hides responsive menu', :aggregate_failures do expect(page).to have_css('.page-title', text: 'Projects') - expect(page).to have_no_text(responsive_menu_text) + expect(page).to have_link('Dashboard', id: 'logo') + + expect(page).to have_no_css('.top-nav-responsive') end end @@ -31,8 +32,22 @@ RSpec.describe 'top nav responsive', :js do it 'hides everything and shows responsive menu', :aggregate_failures do expect(page).to have_no_css('.page-title', text: 'Projects') - expect(page).to have_link('Dashboard', id: 'logo') - expect(page).to have_text(responsive_menu_text) + expect(page).to have_no_link('Dashboard', id: 'logo') + + within '.top-nav-responsive' do + expect(page).to have_link(nil, href: search_path) + expect(page).to have_button('Projects') + expect(page).to have_button('Groups') + expect(page).to have_link('Snippets', href: dashboard_snippets_path) + end + end + + it 'has new dropdown', :aggregate_failures do + click_button('New...') + + expect(page).to have_link('New project', href: new_project_path) + expect(page).to have_link('New group', href: new_group_path) + expect(page).to have_link('New snippet', href: new_snippet_path) end end end diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb index eaef3e6ca28..5019e45593c 100644 --- a/spec/features/projects/environments_pod_logs_spec.rb +++ b/spec/features/projects/environments_pod_logs_spec.rb @@ -40,7 +40,7 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do dropdown_items = find(".dropdown-menu").all(".dropdown-item") expect(dropdown_items.first).to have_content(environment.name) - expect(dropdown_items.size).to eq(3) + expect(dropdown_items.size).to eq(2) end end diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js index b40d9d7d5e2..b107708ac2c 100644 --- a/spec/frontend/logs/components/environment_logs_spec.js +++ b/spec/frontend/logs/components/environment_logs_spec.js @@ -12,7 +12,6 @@ import { mockTrace, mockEnvironmentsEndpoint, mockDocumentationPath, - mockManagedAppsEndpoint, } from '../mock_data'; jest.mock('~/lib/utils/scroll_utils'); @@ -35,7 +34,7 @@ describe('EnvironmentLogs', () => { environmentName: mockEnvName, environmentsPath: mockEnvironmentsEndpoint, clusterApplicationsDocumentationPath: mockDocumentationPath, - clustersPath: mockManagedAppsEndpoint, + clustersPath: '/gitlab-org', }; const updateControlBtnsMock = jest.fn(); diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js index 3fabab4bc59..14c8f7a2ba2 100644 --- a/spec/frontend/logs/mock_data.js +++ b/spec/frontend/logs/mock_data.js @@ -7,8 +7,6 @@ export const mockDocumentationPath = '/documentation.md'; export const mockLogsEndpoint = '/dummy_logs_path.json'; export const mockCursor = 'MOCK_CURSOR'; export const mockNextCursor = 'MOCK_NEXT_CURSOR'; -export const mockManagedAppName = 'kubernetes-cluster-1'; -export const mockManagedAppsEndpoint = `${mockProjectPath}/clusters.json`; const makeMockEnvironment = (id, name, advancedQuerying) => ({ id, @@ -25,31 +23,6 @@ export const mockEnvironments = [ makeMockEnvironment(102, 'review/a-feature', false), ]; -export const mockManagedApps = [ - { - cluster_type: 'project_type', - enabled: true, - environment_scope: '*', - name: 'kubernetes-cluster-1', - provider_type: 'user', - status: 'connected', - path: '/root/autodevops-deploy/-/clusters/15', - gitlab_managed_apps_logs_path: '/root/autodevops-deploy/-/logs?cluster_id=15', - enable_advanced_logs_querying: true, - }, - { - cluster_type: 'project_type', - enabled: true, - environment_scope: '*', - name: 'kubernetes-cluster-2', - provider_type: 'user', - status: 'connected', - path: '/root/autodevops-deploy/-/clusters/16', - gitlab_managed_apps_logs_path: null, - enable_advanced_logs_querying: false, - }, -]; - export const mockPodName = 'production-764c58d697-aaaaa'; export const mockPods = [ mockPodName, diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js index d5118bbde8c..9307a3b62fb 100644 --- a/spec/frontend/logs/stores/actions_spec.js +++ b/spec/frontend/logs/stores/actions_spec.js @@ -11,7 +11,6 @@ import { fetchEnvironments, fetchLogs, fetchMoreLogsPrepend, - fetchManagedApps, } from '~/logs/stores/actions'; import * as types from '~/logs/stores/mutation_types'; import logsPageState from '~/logs/stores/state'; @@ -31,8 +30,6 @@ import { mockResponse, mockCursor, mockNextCursor, - mockManagedApps, - mockManagedAppsEndpoint, } from '../mock_data'; jest.mock('~/flash'); @@ -219,30 +216,6 @@ describe('Logs Store actions', () => { }); }); - describe('fetchManagedApps', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - it('should commit RECEIVE_MANAGED_APPS_DATA_SUCCESS mutation on succesful fetch', () => { - mock.onGet(mockManagedAppsEndpoint).replyOnce(200, { clusters: mockManagedApps }); - return testAction(fetchManagedApps, mockManagedAppsEndpoint, state, [ - { type: types.RECEIVE_MANAGED_APPS_DATA_SUCCESS, payload: mockManagedApps }, - ]); - }); - - it('should commit RECEIVE_MANAGED_APPS_DATA_ERROR on wrong data', () => { - mock.onGet(mockManagedAppsEndpoint).replyOnce(500); - return testAction( - fetchManagedApps, - mockManagedAppsEndpoint, - state, - [{ type: types.RECEIVE_MANAGED_APPS_DATA_ERROR }], - [], - ); - }); - }); - describe('when the backend responds succesfully', () => { let expectedMutations; let expectedActions; diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js index bca1ce4ca92..9d213d8c01f 100644 --- a/spec/frontend/logs/stores/getters_spec.js +++ b/spec/frontend/logs/stores/getters_spec.js @@ -1,14 +1,7 @@ import { trace, showAdvancedFilters } from '~/logs/stores/getters'; import logsPageState from '~/logs/stores/state'; -import { - mockLogsResult, - mockTrace, - mockEnvName, - mockEnvironments, - mockManagedApps, - mockManagedAppName, -} from '../mock_data'; +import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data'; describe('Logs Store getters', () => { let state; @@ -79,43 +72,4 @@ describe('Logs Store getters', () => { }); }); }); - - describe('when no managedApps are set', () => { - beforeEach(() => { - state.environments.current = null; - state.environments.options = []; - state.managedApps.current = mockManagedAppName; - state.managedApps.options = []; - }); - - it('returns false', () => { - expect(showAdvancedFilters(state)).toBe(false); - }); - }); - - describe('when the managedApp supports filters', () => { - beforeEach(() => { - state.environments.current = null; - state.environments.options = mockEnvironments; - state.managedApps.current = mockManagedAppName; - state.managedApps.options = mockManagedApps; - }); - - it('returns true', () => { - expect(showAdvancedFilters(state)).toBe(true); - }); - }); - - describe('when the managedApp does not support filters', () => { - beforeEach(() => { - state.environments.current = null; - state.environments.options = mockEnvironments; - state.managedApps.options = mockManagedApps; - state.managedApps.current = mockManagedApps[1].name; - }); - - it('returns false', () => { - expect(showAdvancedFilters(state)).toBe(false); - }); - }); }); diff --git a/spec/frontend/logs/stores/mutations_spec.js b/spec/frontend/logs/stores/mutations_spec.js index 111c795ba52..988197a8350 100644 --- a/spec/frontend/logs/stores/mutations_spec.js +++ b/spec/frontend/logs/stores/mutations_spec.js @@ -11,8 +11,6 @@ import { mockSearch, mockCursor, mockNextCursor, - mockManagedApps, - mockManagedAppName, } from '../mock_data'; describe('Logs Store Mutations', () => { @@ -32,15 +30,6 @@ describe('Logs Store Mutations', () => { it('sets the environment', () => { mutations[types.SET_PROJECT_ENVIRONMENT](state, mockEnvName); expect(state.environments.current).toEqual(mockEnvName); - expect(state.managedApps.current).toBe(null); - }); - }); - - describe('SET_MANAGED_APP', () => { - it('sets the managed app', () => { - mutations[types.SET_MANAGED_APP](state, mockManagedAppName); - expect(state.managedApps.current).toBe(mockManagedAppName); - expect(state.environments.current).toBe(null); }); }); @@ -265,29 +254,4 @@ describe('Logs Store Mutations', () => { ); }); }); - - describe('RECEIVE_MANAGED_APPS_DATA_SUCCESS', () => { - it('receives managed apps data success', () => { - expect(state.managedApps.options).toEqual([]); - - mutations[types.RECEIVE_MANAGED_APPS_DATA_SUCCESS](state, mockManagedApps); - - expect(state.managedApps.options.length).toEqual(1); - expect(state.managedApps.options).toEqual([mockManagedApps[0]]); - expect(state.managedApps.isLoading).toBe(false); - }); - }); - - describe('RECEIVE_MANAGED_APPS_DATA_ERROR', () => { - it('received managed apps data error', () => { - mutations[types.RECEIVE_MANAGED_APPS_DATA_ERROR](state); - - expect(state.managedApps).toEqual({ - options: [], - isLoading: false, - current: null, - fetchError: true, - }); - }); - }); }); diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js index 1a153b38a05..3274a1b7086 100644 --- a/spec/frontend/nav/components/responsive_app_spec.js +++ b/spec/frontend/nav/components/responsive_app_spec.js @@ -1,7 +1,12 @@ import { shallowMount } from '@vue/test-utils'; import { range } from 'lodash'; import ResponsiveApp from '~/nav/components/responsive_app.vue'; +import ResponsiveHeader from '~/nav/components/responsive_header.vue'; +import ResponsiveHome from '~/nav/components/responsive_home.vue'; +import TopNavContainerView from '~/nav/components/top_nav_container_view.vue'; import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '~/nav/event_hub'; +import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active'; +import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; import { TEST_NAV_DATA } from '../mock_data'; describe('~/nav/components/responsive_app.vue', () => { @@ -12,11 +17,19 @@ describe('~/nav/components/responsive_app.vue', () => { propsData: { navData: TEST_NAV_DATA, }, + stubs: { + KeepAliveSlots, + }, }); }; const triggerResponsiveToggle = () => eventHub.$emit(EVENT_RESPONSIVE_TOGGLE); + const findHome = () => wrapper.findComponent(ResponsiveHome); + const findMobileOverlay = () => wrapper.find('[data-testid="mobile-overlay"]'); + const findSubviewHeader = () => wrapper.findComponent(ResponsiveHeader); + const findSubviewContainer = () => wrapper.findComponent(TopNavContainerView); const hasBodyResponsiveOpen = () => document.body.classList.contains('top-nav-responsive-open'); + const hasMobileOverlayVisible = () => findMobileOverlay().classes('mobile-nav-open'); beforeEach(() => { // Add test class to reset state + assert that we're adding classes correctly @@ -32,6 +45,13 @@ describe('~/nav/components/responsive_app.vue', () => { createComponent(); }); + it('shows home by default', () => { + expect(findHome().isVisible()).toBe(true); + expect(findHome().props()).toEqual({ + navData: resetMenuItemsActive(TEST_NAV_DATA), + }); + }); + it.each` times | expectation ${0} | ${false} @@ -45,6 +65,78 @@ describe('~/nav/components/responsive_app.vue', () => { expect(hasBodyResponsiveOpen()).toBe(expectation); }, ); + + it.each` + events | expectation + ${[]} | ${false} + ${['bv::dropdown::show']} | ${true} + ${['bv::dropdown::show', 'bv::dropdown::hide']} | ${false} + `( + 'with root events $events, movile overlay visible = $expectation', + async ({ events, expectation }) => { + // `await...reduce(async` is like doing an `forEach(async (...))` excpet it works + await events.reduce(async (acc, evt) => { + await acc; + + wrapper.vm.$root.$emit(evt); + + await wrapper.vm.$nextTick(); + }, Promise.resolve()); + + expect(hasMobileOverlayVisible()).toBe(expectation); + }, + ); + }); + + const projectsContainerProps = { + containerClass: 'gl-px-3', + frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.namespace, + frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.vuexModule, + linksPrimary: TEST_NAV_DATA.views.projects.linksPrimary, + linksSecondary: TEST_NAV_DATA.views.projects.linksSecondary, + }; + const groupsContainerProps = { + containerClass: 'gl-px-3', + frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_GROUPS.namespace, + frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_GROUPS.vuexModule, + linksPrimary: TEST_NAV_DATA.views.groups.linksPrimary, + linksSecondary: TEST_NAV_DATA.views.groups.linksSecondary, + }; + + describe.each` + view | header | containerProps + ${'projects'} | ${'Projects'} | ${projectsContainerProps} + ${'groups'} | ${'Groups'} | ${groupsContainerProps} + `('when menu item with $view is clicked', ({ view, header, containerProps }) => { + beforeEach(async () => { + createComponent(); + + findHome().vm.$emit('menu-item-click', { view }); + + await wrapper.vm.$nextTick(); + }); + + it('shows header', () => { + expect(findSubviewHeader().text()).toBe(header); + }); + + it('shows container subview', () => { + expect(findSubviewContainer().props()).toEqual(containerProps); + }); + + it('hides home', () => { + expect(findHome().isVisible()).toBe(false); + }); + + describe('when header back button is clicked', () => { + beforeEach(() => { + findSubviewHeader().vm.$emit('menu-item-click', { view: 'home' }); + }); + + it('shows home', () => { + expect(findHome().isVisible()).toBe(true); + }); + }); }); describe('when destroyed', () => { diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js new file mode 100644 index 00000000000..937c44727c7 --- /dev/null +++ b/spec/frontend/nav/components/responsive_header_spec.js @@ -0,0 +1,67 @@ +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import ResponsiveHeader from '~/nav/components/responsive_header.vue'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; + +const TEST_SLOT_CONTENT = 'Test slot content'; + +describe('~/nav/components/top_nav_menu_sections.vue', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(ResponsiveHeader, { + slots: { + default: TEST_SLOT_CONTENT, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + const findMenuItem = () => wrapper.findComponent(TopNavMenuItem); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders slot', () => { + expect(wrapper.text()).toBe(TEST_SLOT_CONTENT); + }); + + it('renders back button', () => { + const button = findMenuItem(); + + const tooltip = getBinding(button.element, 'gl-tooltip').value.title; + + expect(tooltip).toBe('Go back'); + expect(button.props()).toEqual({ + menuItem: { + id: 'home', + view: 'home', + icon: 'angle-left', + }, + iconOnly: true, + }); + }); + + it('emits nothing', () => { + expect(wrapper.emitted()).toEqual({}); + }); + + describe('when back button is clicked', () => { + beforeEach(() => { + findMenuItem().vm.$emit('click'); + }); + + it('emits menu-item-click', () => { + expect(wrapper.emitted()).toEqual({ + 'menu-item-click': [[{ id: 'home', view: 'home', icon: 'angle-left' }]], + }); + }); + }); +}); diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js new file mode 100644 index 00000000000..8f198d92747 --- /dev/null +++ b/spec/frontend/nav/components/responsive_home_spec.js @@ -0,0 +1,137 @@ +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import ResponsiveHome from '~/nav/components/responsive_home.vue'; +import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue'; +import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue'; +import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue'; +import { TEST_NAV_DATA } from '../mock_data'; + +const TEST_SEARCH_MENU_ITEM = { + id: 'search', + title: 'search', + icon: 'search', + href: '/search', +}; + +const TEST_NEW_DROPDOWN_VIEW_MODEL = { + title: 'new', + menu_sections: [], +}; + +describe('~/nav/components/responsive_home.vue', () => { + let wrapper; + let menuItemClickListener; + + const createComponent = (props = {}) => { + wrapper = shallowMount(ResponsiveHome, { + propsData: { + navData: TEST_NAV_DATA, + ...props, + }, + directives: { + GlTooltip: createMockDirective(), + }, + listeners: { + 'menu-item-click': menuItemClickListener, + }, + }); + }; + + const findSearchMenuItem = () => wrapper.findComponent(TopNavMenuItem); + const findNewDropdown = () => wrapper.findComponent(TopNavNewDropdown); + const findMenuSections = () => wrapper.findComponent(TopNavMenuSections); + + beforeEach(() => { + menuItemClickListener = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + desc | fn + ${'does not show search menu item'} | ${findSearchMenuItem} + ${'does not show new dropdown'} | ${findNewDropdown} + `('$desc', ({ fn }) => { + expect(fn().exists()).toBe(false); + }); + + it('shows menu sections', () => { + expect(findMenuSections().props('sections')).toEqual([ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, + { id: 'secondary', menuItems: TEST_NAV_DATA.secondary }, + ]); + }); + + it('emits when menu sections emits', () => { + expect(menuItemClickListener).not.toHaveBeenCalled(); + + findMenuSections().vm.$emit('menu-item-click', TEST_NAV_DATA.primary[0]); + + expect(menuItemClickListener).toHaveBeenCalledWith(TEST_NAV_DATA.primary[0]); + }); + }); + + describe('without secondary', () => { + beforeEach(() => { + createComponent({ navData: { ...TEST_NAV_DATA, secondary: null } }); + }); + + it('shows menu sections', () => { + expect(findMenuSections().props('sections')).toEqual([ + { id: 'primary', menuItems: TEST_NAV_DATA.primary }, + ]); + }); + }); + + describe('with search view', () => { + beforeEach(() => { + createComponent({ + navData: { + ...TEST_NAV_DATA, + views: { search: TEST_SEARCH_MENU_ITEM }, + }, + }); + }); + + it('shows search menu item', () => { + expect(findSearchMenuItem().props()).toEqual({ + menuItem: TEST_SEARCH_MENU_ITEM, + iconOnly: true, + }); + }); + + it('shows tooltip for search', () => { + const tooltip = getBinding(findSearchMenuItem().element, 'gl-tooltip'); + expect(tooltip.value).toEqual({ title: TEST_SEARCH_MENU_ITEM.title }); + }); + }); + + describe('with new view', () => { + beforeEach(() => { + createComponent({ + navData: { + ...TEST_NAV_DATA, + views: { new: TEST_NEW_DROPDOWN_VIEW_MODEL }, + }, + }); + }); + + it('shows new dropdown', () => { + expect(findNewDropdown().props()).toEqual({ + viewModel: TEST_NEW_DROPDOWN_VIEW_MODEL, + }); + }); + + it('shows tooltip for new dropdown', () => { + const tooltip = getBinding(findNewDropdown().element, 'gl-tooltip'); + expect(tooltip.value).toEqual({ title: TEST_NEW_DROPDOWN_VIEW_MODEL.title }); + }); + }); +}); diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js index 45c243191d8..06d2179b859 100644 --- a/spec/frontend/nav/components/top_nav_container_view_spec.js +++ b/spec/frontend/nav/components/top_nav_container_view_spec.js @@ -13,6 +13,7 @@ const DEFAULT_PROPS = { frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule, linksPrimary: TEST_NAV_DATA.primary, linksSecondary: TEST_NAV_DATA.secondary, + containerClass: 'test-frequent-items-container-class', }; const TEST_OTHER_PROPS = { namespace: 'projects', @@ -44,6 +45,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => { attributes: parent.findComponent(FrequentItemsApp).attributes(), }; }; + const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]'); afterEach(() => { wrapper.destroy(); @@ -85,6 +87,10 @@ describe('~/nav/components/top_nav_container_view.vue', () => { }); }); + it('renders given container class', () => { + expect(findFrequentItemsContainer().classes(DEFAULT_PROPS.containerClass)).toBe(true); + }); + it('renders menu sections', () => { const sections = [ { id: 'primary', menuItems: TEST_NAV_DATA.primary }, diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js index 37b58412389..fd2b4d3b056 100644 --- a/spec/frontend/nav/components/top_nav_menu_item_spec.js +++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js @@ -30,7 +30,10 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { const findButtonIcons = () => findButton() .findAllComponents(GlIcon) - .wrappers.map((x) => x.props('name')); + .wrappers.map((x) => ({ + name: x.props('name'), + classes: x.classes(), + })); beforeEach(() => { listener = jest.fn(); @@ -65,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { expect(listener).toHaveBeenCalledWith('TEST'); }); + + it('renders expected icons', () => { + expect(findButtonIcons()).toEqual([ + { + name: TEST_MENU_ITEM.icon, + classes: ['gl-mr-2!'], + }, + { + name: 'chevron-right', + classes: ['gl-ml-auto'], + }, + ]); + }); + }); + + describe('with icon-only', () => { + beforeEach(() => { + createComponent({ iconOnly: true }); + }); + + it('does not render title or view icon', () => { + expect(wrapper.text()).toBe(''); + }); + + it('only renders menuItem icon', () => { + expect(findButtonIcons()).toEqual([ + { + name: TEST_MENU_ITEM.icon, + classes: [], + }, + ]); + }); }); describe.each` desc | menuItem | expectedIcons - ${'default'} | ${TEST_MENU_ITEM} | ${[TEST_MENU_ITEM.icon, 'chevron-right']} ${'with no icon'} | ${{ ...TEST_MENU_ITEM, icon: null }} | ${['chevron-right']} ${'with no view'} | ${{ ...TEST_MENU_ITEM, view: null }} | ${[TEST_MENU_ITEM.icon]} ${'with no icon or view'} | ${{ ...TEST_MENU_ITEM, view: null, icon: null }} | ${[]} @@ -79,7 +113,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { }); it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => { - expect(findButtonIcons()).toEqual(expectedIcons); + expect(findButtonIcons().map((x) => x.name)).toEqual(expectedIcons); }); }); diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js index a6739507915..d56542fe572 100644 --- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js +++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js @@ -51,11 +51,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { menuItems: [ { menuItem: TEST_SECTIONS[0].menuItems[0], - classes: [], + classes: ['gl-w-full'], }, ...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({ menuItem, - classes: ['gl-mt-1'], + classes: ['gl-w-full', 'gl-mt-1'], })), ], }, @@ -64,11 +64,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { menuItems: [ { menuItem: TEST_SECTIONS[1].menuItems[0], - classes: [], + classes: ['gl-w-full'], }, ...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({ menuItem, - classes: ['gl-mt-1'], + classes: ['gl-w-full', 'gl-mt-1'], })), ], }, diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js new file mode 100644 index 00000000000..18210658b89 --- /dev/null +++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js @@ -0,0 +1,122 @@ +import { GlDropdown } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue'; + +const TEST_VIEW_MODEL = { + title: 'Dropdown', + menu_sections: [ + { + title: 'Section 1', + menu_items: [ + { id: 'foo-1', title: 'Foo 1', href: '/foo/1' }, + { id: 'foo-2', title: 'Foo 2', href: '/foo/2' }, + { id: 'foo-3', title: 'Foo 3', href: '/foo/3' }, + ], + }, + { + title: 'Section 2', + menu_items: [ + { id: 'bar-1', title: 'Bar 1', href: '/bar/1' }, + { id: 'bar-2', title: 'Bar 2', href: '/bar/2' }, + ], + }, + ], +}; + +describe('~/nav/components/top_nav_menu_sections.vue', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(TopNavNewDropdown, { + propsData: { + viewModel: TEST_VIEW_MODEL, + ...props, + }, + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownContents = () => + findDropdown() + .findAll('[data-testid]') + .wrappers.map((child) => { + const type = child.attributes('data-testid'); + + if (type === 'divider') { + return { type }; + } else if (type === 'header') { + return { type, text: child.text() }; + } + + return { + type, + text: child.text(), + href: child.attributes('href'), + }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders dropdown parent', () => { + expect(findDropdown().props()).toMatchObject({ + text: TEST_VIEW_MODEL.title, + textSrOnly: true, + icon: 'plus', + }); + }); + + it('renders dropdown content', () => { + expect(findDropdownContents()).toEqual([ + { + type: 'header', + text: TEST_VIEW_MODEL.menu_sections[0].title, + }, + ...TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({ + type: 'item', + href, + text: title, + })), + { + type: 'divider', + }, + { + type: 'header', + text: TEST_VIEW_MODEL.menu_sections[1].title, + }, + ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({ + type: 'item', + href, + text: title, + })), + ]); + }); + }); + + describe('with only 1 section', () => { + beforeEach(() => { + createComponent({ + viewModel: { + ...TEST_VIEW_MODEL, + menu_sections: TEST_VIEW_MODEL.menu_sections.slice(0, 1), + }, + }); + }); + + it('renders dropdown content without headers and dividers', () => { + expect(findDropdownContents()).toEqual( + TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({ + type: 'item', + href, + text: title, + })), + ); + }); + }); +}); diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js index 2987d8deb16..c2ad86a4605 100644 --- a/spec/frontend/nav/mock_data.js +++ b/spec/frontend/nav/mock_data.js @@ -25,11 +25,15 @@ export const TEST_NAV_DATA = { namespace: 'projects', currentUserName: '', currentItem: {}, + linksPrimary: [{ id: 'project-link', href: '/path/to/projects', title: 'Project Link' }], + linksSecondary: [], }, groups: { namespace: 'groups', currentUserName: '', currentItem: {}, + linksPrimary: [], + linksSecondary: [{ id: 'group-link', href: '/path/to/groups', title: 'Group Link' }], }, }, }; diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js index 2cc8b2d389d..9da370747fc 100644 --- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js @@ -532,7 +532,7 @@ describe('MrWidgetOptions', () => { nextTick(() => { const tooltip = wrapper.find('[data-testid="question-o-icon"]'); - expect(wrapper.text()).toContain('Deletes source branch'); + expect(wrapper.text()).toContain('The source branch will be deleted'); expect(tooltip.attributes('title')).toBe( 'A user with write access to the source branch selected this option', ); @@ -548,7 +548,7 @@ describe('MrWidgetOptions', () => { nextTick(() => { expect(wrapper.text()).toContain('The source branch has been deleted'); - expect(wrapper.text()).not.toContain('Deletes source branch'); + expect(wrapper.text()).not.toContain('The source branch will be deleted'); done(); }); diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb index 006d6785506..d89beb183f4 100644 --- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb @@ -10,15 +10,15 @@ RSpec.describe Resolvers::Ci::RunnersResolver do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:inactive_project_runner) do - create(:ci_runner, :project, projects: [project], active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner)) + create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner)) end let_it_be(:offline_project_runner) do - create(:ci_runner, :project, projects: [project], contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner)) + create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner)) end - let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], contacted_at: 1.second.ago) } - let_it_be(:instance_runner) { create(:ci_runner, :instance, contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) } + let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 1.second.ago) } + let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) } describe '#resolve' do subject { resolve(described_class, ctx: { current_user: user }, args: args).items.to_a } @@ -27,6 +27,14 @@ RSpec.describe Resolvers::Ci::RunnersResolver do {} end + context 'when the user cannot see runners' do + let(:user) { create(:user) } + + it 'returns no runners' do + is_expected.to be_empty + end + end + context 'without sort' do it 'returns all the runners' do is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner) @@ -132,5 +140,35 @@ RSpec.describe Resolvers::Ci::RunnersResolver do end end end + + context 'when text is filtered' do + let(:args) do + { search: search_term } + end + + context 'to "project"' do + let(:search_term) { 'project' } + + it 'returns both project runners' do + is_expected.to contain_exactly(inactive_project_runner, offline_project_runner) + end + end + + context 'to "group"' do + let(:search_term) { 'group' } + + it 'returns group runner' do + is_expected.to contain_exactly(group_runner) + end + end + + context 'to "defghi"' do + let(:search_term) { 'defghi' } + + it 'returns runners containing term in token' do + is_expected.to contain_exactly(offline_project_runner) + end + end + end end end diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb index 9426f39cd80..b9ab6f9d8fd 100644 --- a/spec/helpers/nav/top_nav_helper_spec.rb +++ b/spec/helpers/nav/top_nav_helper_spec.rb @@ -5,11 +5,16 @@ require 'spec_helper' RSpec.describe Nav::TopNavHelper do include ActionView::Helpers::UrlHelper - describe '#top_nav_view_model' do - let_it_be(:user) { build_stubbed(:user) } - let_it_be(:admin) { build_stubbed(:user, :admin) } + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:admin) { build_stubbed(:user, :admin) } + + let(:current_user) { nil } + + before do + allow(helper).to receive(:current_user) { current_user } + end - let(:current_user) { nil } + describe '#top_nav_view_model' do let(:current_project) { nil } let(:current_group) { nil } let(:with_current_settings_admin_mode) { false } @@ -26,7 +31,6 @@ RSpec.describe Nav::TopNavHelper do let(:active_title) { 'Menu' } before do - allow(helper).to receive(:current_user) { current_user } allow(Gitlab::CurrentSettings).to receive(:admin_mode) { with_current_settings_admin_mode } allow(helper).to receive(:header_link?).with(:admin_mode) { with_header_link_admin_mode } allow(Gitlab::Sherlock).to receive(:enabled?) { with_sherlock_enabled } @@ -487,4 +491,50 @@ RSpec.describe Nav::TopNavHelper do end end end + + describe '#top_nav_responsive_view_model' do + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + + let(:with_search) { false } + let(:with_new_view_model) { nil } + + let(:subject) { helper.top_nav_responsive_view_model(project: project, group: group) } + + before do + allow(helper).to receive(:header_link?).with(:search) { with_search } + allow(helper).to receive(:new_dropdown_view_model).with(project: project, group: group) { with_new_view_model } + end + + it 'has nil new subview' do + expect(subject[:views][:new]).to be_nil + end + + it 'has nil search subview' do + expect(subject[:views][:search]).to be_nil + end + + context 'with search' do + let(:with_search) { true } + + it 'has search subview' do + expect(subject[:views][:search]).to eq( + ::Gitlab::Nav::TopNavMenuItem.build( + id: 'search', + title: 'Search', + icon: 'search', + href: search_path + ) + ) + end + end + + context 'with new' do + let(:with_new_view_model) { { id: 'test-new-view-model' } } + + it 'has new subview' do + expect(subject[:views][:new]).to eq({ id: 'test-new-view-model' }) + end + end + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index ab7076e037e..61f80bd43b1 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -873,12 +873,12 @@ RSpec.describe Ci::Runner do expect(described_class.search(runner.token)).to eq([runner]) end - it 'returns runners with a partially matching token' do - expect(described_class.search(runner.token[0..2])).to eq([runner]) + it 'does not return runners with a partially matching token' do + expect(described_class.search(runner.token[0..2])).to be_empty end - it 'returns runners with a matching token regardless of the casing' do - expect(described_class.search(runner.token.upcase)).to eq([runner]) + it 'does not return runners with a matching token with different casing' do + expect(described_class.search(runner.token.upcase)).to be_empty end it 'returns runners with a matching description' do diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index b8bf4f9683c..6e9d02b157b 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -757,19 +757,8 @@ RSpec.describe Packages::Package, type: :model do end end - context 'with arel scope feature flag enabled' do - it_behaves_like 'order_project_path scope' - it_behaves_like 'order_project_path_desc scope' - end - - context 'with feature flag disabled' do - before do - stub_feature_flags(arel_package_scopes: false) - end - - it_behaves_like 'order_project_path scope' - it_behaves_like 'order_project_path_desc scope' - end + it_behaves_like 'order_project_path scope' + it_behaves_like 'order_project_path_desc scope' end describe '.order_by_package_file' do diff --git a/spec/requests/api/group_avatar_spec.rb b/spec/requests/api/group_avatar_spec.rb new file mode 100644 index 00000000000..be5cfbc234c --- /dev/null +++ b/spec/requests/api/group_avatar_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::GroupAvatar do + def avatar_path(group) + "/groups/#{group.id}/avatar" + end + + describe 'GET /groups/:id/avatar' do + context 'when the group is public' do + it 'retrieves the avatar successfully' do + group = create(:group, :public, :with_avatar) + + get api(avatar_path(group)) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the group does not have avatar' do + it 'returns :not_found' do + group = create(:group, :public) + + get api(avatar_path(group)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when the group is private' do + let(:group) { create(:group, :private, :with_avatar) } + + context 'when the user is not authenticated' do + it 'returns :not_found' do + get api(avatar_path(group)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the the group user is authenticated' do + context 'and have access to the group' do + it 'retrieves the avatar successfully' do + owner = create(:user) + group.add_owner(owner) + + get api(avatar_path(group), owner) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'and does not have access to the group' do + it 'returns :not_found' do + get api(avatar_path(group), create(:user)) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + end +end diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb index c3cdae2cd21..1967d0ba8b1 100644 --- a/spec/requests/oauth/tokens_controller_spec.rb +++ b/spec/requests/oauth/tokens_controller_spec.rb @@ -3,12 +3,71 @@ require 'spec_helper' RSpec.describe Oauth::TokensController do - it 'allows cross-origin POST requests' do - post '/oauth/token', headers: { 'Origin' => 'http://notgitlab.com' } + let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } } + let(:other_headers) { {} } + let(:headers) { cors_request_headers.merge(other_headers)} - expect(response.headers['Access-Control-Allow-Origin']).to eq '*' - expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST' - expect(response.headers['Access-Control-Allow-Headers']).to be_nil - expect(response.headers['Access-Control-Allow-Credentials']).to be_nil + shared_examples 'cross-origin POST request' do + it 'allows cross-origin requests' do + expect(response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST' + expect(response.headers['Access-Control-Allow-Headers']).to be_nil + expect(response.headers['Access-Control-Allow-Credentials']).to be_nil + end + end + + shared_examples 'CORS preflight OPTIONS request' do + it 'returns 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'allows cross-origin requests' do + expect(response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST' + expect(response.headers['Access-Control-Allow-Headers']).to eq 'Authorization' + expect(response.headers['Access-Control-Allow-Credentials']).to be_nil + end + end + + describe 'POST /oauth/token' do + before do + post '/oauth/token', headers: headers + end + + it_behaves_like 'cross-origin POST request' + end + + describe 'OPTIONS /oauth/token' do + let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } } + + before do + options '/oauth/token', headers: headers + end + + it_behaves_like 'CORS preflight OPTIONS request' + end + + describe 'POST /oauth/revoke' do + let(:other_headers) { { 'Content-Type' => 'application/x-www-form-urlencoded' } } + + before do + post '/oauth/revoke', headers: headers, params: { token: '12345' } + end + + it 'returns 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it_behaves_like 'cross-origin POST request' + end + + describe 'OPTIONS /oauth/revoke' do + let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } } + + before do + options '/oauth/revoke', headers: headers + end + + it_behaves_like 'CORS preflight OPTIONS request' end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 7b682d76150..5bf786f2290 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -41,6 +41,8 @@ RSpec.describe 'OpenID Connect requests' do } end + let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } } + def request_access_token! login_as user @@ -81,6 +83,24 @@ RSpec.describe 'OpenID Connect requests' do end end + shared_examples 'cross-origin GET request' do + it 'allows cross-origin request' do + expect(response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, HEAD' + expect(response.headers['Access-Control-Allow-Headers']).to be_nil + expect(response.headers['Access-Control-Allow-Credentials']).to be_nil + end + end + + shared_examples 'cross-origin GET and POST request' do + it 'allows cross-origin request' do + expect(response.headers['Access-Control-Allow-Origin']).to eq '*' + expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, HEAD, POST' + expect(response.headers['Access-Control-Allow-Headers']).to be_nil + expect(response.headers['Access-Control-Allow-Credentials']).to be_nil + end + end + context 'Application with OpenID scope' do let(:application) { create :oauth_application, scopes: 'openid' } @@ -180,6 +200,51 @@ RSpec.describe 'OpenID Connect requests' do expect(response).to redirect_to('/users/sign_in') end end + + context 'OpenID Discovery keys' do + context 'with a cross-origin request' do + before do + get '/oauth/discovery/keys', headers: cors_request_headers + end + + it 'returns data' do + expect(response).to have_gitlab_http_status(:ok) + end + + it_behaves_like 'cross-origin GET request' + end + + context 'with a cross-origin preflight OPTIONS request' do + before do + options '/oauth/discovery/keys', headers: cors_request_headers + end + + it_behaves_like 'cross-origin GET request' + end + end + + context 'OpenID WebFinger endpoint' do + context 'with a cross-origin request' do + before do + get '/.well-known/webfinger', headers: cors_request_headers, params: { resource: 'user@example.com' } + end + + it 'returns data' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['subject']).to eq('user@example.com') + end + + it_behaves_like 'cross-origin GET request' + end + end + + context 'with a cross-origin preflight OPTIONS request' do + before do + options '/.well-known/webfinger', headers: cors_request_headers, params: { resource: 'user@example.com' } + end + + it_behaves_like 'cross-origin GET request' + end end context 'OpenID configuration information' do @@ -191,6 +256,27 @@ RSpec.describe 'OpenID Connect requests' do expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email]) end + + context 'with a cross-origin request' do + before do + get '/.well-known/openid-configuration', headers: cors_request_headers + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['issuer']).to eq('http://localhost') + expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') + expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email]) + end + + it_behaves_like 'cross-origin GET request' + end + + context 'with a cross-origin preflight OPTIONS request' do + before do + options '/.well-known/openid-configuration', headers: cors_request_headers + end + + it_behaves_like 'cross-origin GET request' + end end context 'Application with OpenID and email scopes' do @@ -218,6 +304,30 @@ RSpec.describe 'OpenID Connect requests' do it 'has true in email_verified claim' do expect(json_response['email_verified']).to eq(true) end + + context 'with a cross-origin request' do + before do + get '/oauth/userinfo', headers: cors_request_headers + end + + it_behaves_like 'cross-origin GET and POST request' + end + + context 'with a cross-origin POST request' do + before do + post '/oauth/userinfo', headers: cors_request_headers + end + + it_behaves_like 'cross-origin GET and POST request' + end + + context 'with a cross-origin preflight OPTIONS request' do + before do + options '/oauth/userinfo', headers: cors_request_headers + end + + it_behaves_like 'cross-origin GET and POST request' + end end context 'ID token payload' do |