diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-09 03:07:14 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-09 03:07:14 +0000 |
commit | 8e7172c40e205a27c204de448ac4fe0551ffa1af (patch) | |
tree | c8a1824a1793268daa166e82a2eda5da71a8f15f /spec | |
parent | 496e1ea9716836b3d8cb0d556d23692c0e68976b (diff) | |
download | gitlab-ce-8e7172c40e205a27c204de448ac4fe0551ffa1af.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/db/schema_spec.rb | 4 | ||||
-rw-r--r-- | spec/features/markdown/copy_as_gfm_spec.rb | 8 | ||||
-rw-r--r-- | spec/fixtures/config/redis_cluster_format_host.yml | 29 | ||||
-rw-r--r-- | spec/frontend/__helpers__/mock_window_location_helper.js | 9 | ||||
-rw-r--r-- | spec/frontend/ide/init_gitlab_web_ide_spec.js | 66 | ||||
-rw-r--r-- | spec/frontend/ide/remote/index_spec.js | 91 | ||||
-rw-r--r-- | spec/frontend/lib/utils/create_and_submit_form_spec.js | 73 | ||||
-rw-r--r-- | spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js | 3 | ||||
-rw-r--r-- | spec/helpers/ide_helper_spec.rb | 5 | ||||
-rw-r--r-- | spec/helpers/nav/top_nav_helper_spec.rb | 24 | ||||
-rw-r--r-- | spec/helpers/wiki_helper_spec.rb | 2 | ||||
-rw-r--r-- | spec/requests/web_ide/remote_ide_controller_spec.rb | 141 | ||||
-rw-r--r-- | spec/routing/web_ide_routing_spec.rb | 22 | ||||
-rw-r--r-- | spec/support/redis/redis_shared_examples.rb | 55 |
14 files changed, 502 insertions, 30 deletions
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 66dfc38ad26..61119afae34 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -9,9 +9,7 @@ RSpec.describe 'Database schema' do let(:tables) { connection.tables } let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb } - IGNORED_INDEXES_ON_FKS = { - issues: %w[work_item_type_id] - }.with_indifferent_access.freeze + IGNORED_INDEXES_ON_FKS = {}.with_indifferent_access.freeze TABLE_PARTITIONS = %w[ci_builds_metadata].freeze diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index 648dcc9b5df..8073e7e9556 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -459,25 +459,25 @@ RSpec.describe 'Copy as GFM', :js, feature_category: :team_planning do </a> </div> <!----> - <button type="button" class="btn qa-apply-btn js-apply-btn">Apply suggestion</button> + <button type="button" class="btn js-apply-btn">Apply suggestion</button> </div> <table class="mb-3 md-suggestion-diff js-syntax-highlight code white"> <tbody> <tr class="line_holder old"> - <td class="diff-line-num old_line qa-old-diff-line-number old">9</td> + <td class="diff-line-num old_line old">9</td> <td class="diff-line-num new_line old"></td> <td class="line_content old"><span>Old </span></td> </tr> <tr class="line_holder new"> <td class="diff-line-num old_line new"></td> - <td class="diff-line-num new_line qa-new-diff-line-number new">9</td> + <td class="diff-line-num new_line new">9</td> <td class="line_content new"><span>New </span></td> </tr> <tr class="line_holder new"> <td class="diff-line-num old_line new"></td> - <td class="diff-line-num new_line qa-new-diff-line-number new">10</td> + <td class="diff-line-num new_line new">10</td> <td class="line_content new"><span> And newer </span></td> </tr> diff --git a/spec/fixtures/config/redis_cluster_format_host.yml b/spec/fixtures/config/redis_cluster_format_host.yml new file mode 100644 index 00000000000..7303db72c4e --- /dev/null +++ b/spec/fixtures/config/redis_cluster_format_host.yml @@ -0,0 +1,29 @@ +# redis://[:password@]host[:port][/db-number][?option=value] +# more details: http://www.iana.org/assignments/uri-schemes/prov/redis +development: + password: myclusterpassword + cluster: + - + host: development-master1 + port: 6379 + - + host: development-master2 + port: 6379 +test: + password: myclusterpassword + cluster: + - + host: test-master1 + port: 6379 + - + host: test-master2 + port: 6379 +production: + password: myclusterpassword + cluster: + - + host: production-master1 + port: 6379 + - + host: production-master2 + port: 6379 diff --git a/spec/frontend/__helpers__/mock_window_location_helper.js b/spec/frontend/__helpers__/mock_window_location_helper.js index 14082857053..a923ca661c5 100644 --- a/spec/frontend/__helpers__/mock_window_location_helper.js +++ b/spec/frontend/__helpers__/mock_window_location_helper.js @@ -27,12 +27,19 @@ const useMockLocation = (fn) => { * Create an object with the location interface but `jest.fn()` implementations. */ export const createWindowLocationSpy = () => { - return { + const { origin, href } = window.location; + + const mockLocation = { assign: jest.fn(), reload: jest.fn(), replace: jest.fn(), toString: jest.fn(), + origin, + // TODO: Do we need to update `origin` if `href` is changed? + href, }; + + return mockLocation; }; /** diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js index ae4d8d6d947..3b5b1ede28c 100644 --- a/spec/frontend/ide/init_gitlab_web_ide_spec.js +++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js @@ -1,8 +1,13 @@ import { start } from '@gitlab/web-ide'; import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action'; +import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form'; import { TEST_HOST } from 'helpers/test_constants'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('@gitlab/web-ide'); +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action'); +jest.mock('~/lib/utils/create_and_submit_form'); const ROOT_ELEMENT_ID = 'ide'; const TEST_NONCE = 'test123nonce'; @@ -10,8 +15,16 @@ const TEST_PROJECT_PATH = 'group1/project1'; const TEST_BRANCH_NAME = '12345-foo-patch'; const TEST_GITLAB_URL = 'https://test-gitlab/'; const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path'; +const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path'; +const TEST_START_REMOTE_PARAMS = { + remoteHost: 'dev.example.gitlab.com/test', + remotePath: '/test/projects/f oo', + connectionToken: '123abc', +}; describe('ide/init_gitlab_web_ide', () => { + let resolveConfirm; + const createRootElement = () => { const el = document.createElement('div'); @@ -21,19 +34,32 @@ describe('ide/init_gitlab_web_ide', () => { el.dataset.projectPath = TEST_PROJECT_PATH; el.dataset.cspNonce = TEST_NONCE; el.dataset.branchName = TEST_BRANCH_NAME; + el.dataset.ideRemotePath = TEST_IDE_REMOTE_PATH; document.body.append(el); }; const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID); - const act = () => initGitlabWebIDE(findRootElement()); + const createSubject = () => initGitlabWebIDE(findRootElement()); + const triggerHandleStartRemote = (startRemoteParams) => { + const [, config] = start.mock.calls[0]; + + config.handleStartRemote(startRemoteParams); + }; beforeEach(() => { process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH; window.gon.gitlab_url = TEST_GITLAB_URL; + confirmAction.mockImplementation( + () => + new Promise((resolve) => { + resolveConfirm = resolve; + }), + ); + createRootElement(); - act(); + createSubject(); }); afterEach(() => { @@ -48,6 +74,7 @@ describe('ide/init_gitlab_web_ide', () => { ref: TEST_BRANCH_NAME, gitlabUrl: TEST_GITLAB_URL, nonce: TEST_NONCE, + handleStartRemote: expect.any(Function), }); }); @@ -59,4 +86,39 @@ describe('ide/init_gitlab_web_ide', () => { '<div id="ide" class="gl--flex-center gl-relative gl-h-full"></div>', ); }); + + describe('when handleStartRemote is triggered', () => { + beforeEach(() => { + triggerHandleStartRemote(TEST_START_REMOTE_PARAMS); + }); + + it('promts for confirm', () => { + expect(confirmAction).toHaveBeenCalledWith(expect.any(String), { + primaryBtnText: expect.any(String), + cancelBtnText: expect.any(String), + }); + }); + + it('does not submit, when not confirmed', async () => { + resolveConfirm(false); + + await waitForPromises(); + + expect(createAndSubmitForm).not.toHaveBeenCalled(); + }); + + it('submits, when confirmed', async () => { + resolveConfirm(true); + + await waitForPromises(); + + expect(createAndSubmitForm).toHaveBeenCalledWith({ + url: '/-/ide/remote/dev.example.gitlab.com%2Ftest/test/projects/f%20oo', + data: { + connection_token: TEST_START_REMOTE_PARAMS.connectionToken, + return_url: window.location.href, + }, + }); + }); + }); }); diff --git a/spec/frontend/ide/remote/index_spec.js b/spec/frontend/ide/remote/index_spec.js new file mode 100644 index 00000000000..0f23b0a4e45 --- /dev/null +++ b/spec/frontend/ide/remote/index_spec.js @@ -0,0 +1,91 @@ +import { startRemote } from '@gitlab/web-ide'; +import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide'; +import { mountRemoteIDE } from '~/ide/remote'; +import { TEST_HOST } from 'helpers/test_constants'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; + +jest.mock('@gitlab/web-ide'); +jest.mock('~/ide/lib/gitlab_web_ide'); + +const TEST_DATA = { + remoteHost: 'example.com:3443', + remotePath: 'test/path/gitlab', + cspNonce: 'just7some8noncense', + connectionToken: 'connectAtoken', + returnUrl: 'https://example.com/return', +}; + +const TEST_BASE_CONFIG = { + gitlabUrl: '/test/gitlab', +}; + +const TEST_RETURN_URL_SAME_ORIGIN = `${TEST_HOST}/foo/example`; + +describe('~/ide/remote/index', () => { + useMockLocationHelper(); + const originalHref = window.location.href; + + let el; + let rootEl; + + beforeEach(() => { + el = document.createElement('div'); + Object.entries(TEST_DATA).forEach(([key, value]) => { + el.dataset[key] = value; + }); + + // Stub setupRootElement so we can assert on return element + rootEl = document.createElement('div'); + setupRootElement.mockReturnValue(rootEl); + + // Stub getBaseConfig so we can assert + getBaseConfig.mockReturnValue(TEST_BASE_CONFIG); + }); + + describe('default', () => { + beforeEach(() => { + mountRemoteIDE(el); + }); + + it('calls startRemote', () => { + expect(startRemote).toHaveBeenCalledWith(rootEl, { + ...TEST_BASE_CONFIG, + nonce: TEST_DATA.cspNonce, + connectionToken: TEST_DATA.connectionToken, + remoteAuthority: `/${TEST_DATA.remoteHost}`, + hostPath: `/${TEST_DATA.remotePath}`, + handleError: expect.any(Function), + handleClose: expect.any(Function), + }); + }); + }); + + describe.each` + returnUrl | fnName | reloadExpectation | hrefExpectation + ${TEST_DATA.returnUrl} | ${'handleError'} | ${1} | ${originalHref} + ${TEST_DATA.returnUrl} | ${'handleClose'} | ${1} | ${originalHref} + ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleClose'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN} + ${TEST_RETURN_URL_SAME_ORIGIN} | ${'handleError'} | ${0} | ${TEST_RETURN_URL_SAME_ORIGIN} + ${''} | ${'handleClose'} | ${1} | ${originalHref} + `( + 'with returnUrl=$returnUrl and fn=$fnName', + ({ returnUrl, fnName, reloadExpectation, hrefExpectation }) => { + beforeEach(() => { + el.dataset.returnUrl = returnUrl; + + mountRemoteIDE(el); + }); + + it('changes location', () => { + expect(window.location.reload).not.toHaveBeenCalled(); + + const [, config] = startRemote.mock.calls[0]; + + config[fnName](); + + expect(window.location.reload).toHaveBeenCalledTimes(reloadExpectation); + expect(window.location.href).toBe(hrefExpectation); + }); + }, + ); +}); diff --git a/spec/frontend/lib/utils/create_and_submit_form_spec.js b/spec/frontend/lib/utils/create_and_submit_form_spec.js new file mode 100644 index 00000000000..9f2472c60f7 --- /dev/null +++ b/spec/frontend/lib/utils/create_and_submit_form_spec.js @@ -0,0 +1,73 @@ +import csrf from '~/lib/utils/csrf'; +import { TEST_HOST } from 'helpers/test_constants'; +import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form'; +import { joinPaths } from '~/lib/utils/url_utility'; + +const TEST_URL = '/foo/bar/lorem'; +const TEST_DATA = { + 'test_thing[0]': 'Lorem Ipsum', + 'test_thing[1]': 'Dolar Sit', + x: 123, +}; +const TEST_CSRF = 'testcsrf00=='; + +describe('~/lib/utils/create_and_submit_form', () => { + let submitSpy; + + const findForm = () => document.querySelector('form'); + const findInputsModel = () => + Array.from(findForm().querySelectorAll('input')).map((inputEl) => ({ + type: inputEl.type, + name: inputEl.name, + value: inputEl.value, + })); + + beforeEach(() => { + submitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit'); + document.head.innerHTML = `<meta name="csrf-token" content="${TEST_CSRF}">`; + csrf.init(); + }); + + afterEach(() => { + document.head.innerHTML = ''; + document.body.innerHTML = ''; + }); + + describe('default', () => { + beforeEach(() => { + createAndSubmitForm({ + url: TEST_URL, + data: TEST_DATA, + }); + }); + + it('creates form', () => { + const form = findForm(); + + expect(form.action).toBe(joinPaths(TEST_HOST, TEST_URL)); + expect(form.method).toBe('post'); + expect(form.style).toMatchObject({ + display: 'none', + }); + }); + + it('creates inputs', () => { + expect(findInputsModel()).toEqual([ + ...Object.keys(TEST_DATA).map((key) => ({ + type: 'hidden', + name: key, + value: String(TEST_DATA[key]), + })), + { + type: 'hidden', + name: 'authenticity_token', + value: TEST_CSRF, + }, + ]); + }); + + it('submits form', () => { + expect(submitSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js index 8e08864bdb8..cbb5aa52694 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js @@ -232,6 +232,7 @@ describe('Container Expiration Policy Settings Form', () => { describe('form', () => { describe('form submit event', () => { useMockLocationHelper(); + const originalHref = window.location.href; it('save has type submit', () => { mountComponent(); @@ -319,7 +320,7 @@ describe('Container Expiration Policy Settings Form', () => { await submitForm(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE); - expect(window.location.href).toBeUndefined(); + expect(window.location.href).toBe(originalHref); }); it('parses the error messages', async () => { diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index 447967fd345..06ea1f40eab 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe IdeHelper do +RSpec.describe IdeHelper, feature_category: :web_ide do describe '#ide_data' do let_it_be(:project) { create(:project) } let_it_be(:user) { project.creator } @@ -30,7 +30,8 @@ RSpec.describe IdeHelper do help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'), 'branch-name' => 'master', 'project-path' => project.path_with_namespace, - 'csp-nonce' => 'test-csp-nonce' + 'csp-nonce' => 'test-csp-nonce', + 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path') ) end diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb index 0d43cfaae90..c4a8536032e 100644 --- a/spec/helpers/nav/top_nav_helper_spec.rb +++ b/spec/helpers/nav/top_nav_helper_spec.rb @@ -122,10 +122,10 @@ RSpec.describe Nav::TopNavHelper do title: 'Switch to' ) expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( - css_class: 'qa-projects-dropdown', data: { track_action: 'click_dropdown', - track_label: 'projects_dropdown' + track_label: 'projects_dropdown', + qa_selector: 'projects_dropdown' }, icon: 'project', id: 'project', @@ -219,10 +219,10 @@ RSpec.describe Nav::TopNavHelper do title: 'Switch to' ) expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( - css_class: 'qa-groups-dropdown', data: { track_action: 'click_dropdown', - track_label: 'groups_dropdown' + track_label: 'groups_dropdown', + qa_selector: 'groups_dropdown' }, icon: 'group', id: 'groups', @@ -323,10 +323,7 @@ RSpec.describe Nav::TopNavHelper do title: 'Explore' ) expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( - data: { - qa_selector: 'milestones_link', - **menu_data_tracking_attrs('milestones') - }, + data: { **menu_data_tracking_attrs('milestones') }, href: '/dashboard/milestones', icon: 'clock', id: 'milestones', @@ -385,10 +382,7 @@ RSpec.describe Nav::TopNavHelper do title: 'Explore' ) expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( - data: { - qa_selector: 'activity_link', - **menu_data_tracking_attrs('activity') - }, + data: { **menu_data_tracking_attrs('activity') }, href: '/dashboard/activity', icon: 'history', id: 'activity', @@ -417,15 +411,13 @@ RSpec.describe Nav::TopNavHelper do it 'has admin as first :secondary item' do expected_admin_item = ::Gitlab::Nav::TopNavMenuItem.build( data: { - qa_selector: 'menu_item_link', - qa_title: 'Admin', + qa_selector: 'admin_area_link', **menu_data_tracking_attrs('admin') }, id: 'admin', title: 'Admin', icon: 'admin', - href: '/admin', - css_class: 'qa-admin-area-link' + href: '/admin' ) expect(subject[:secondary].first).to eq(expected_admin_item) diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb index 59624dc0682..497cd5d1e7f 100644 --- a/spec/helpers/wiki_helper_spec.rb +++ b/spec/helpers/wiki_helper_spec.rb @@ -76,7 +76,7 @@ RSpec.describe WikiHelper do describe '#wiki_sort_controls' do let(:wiki) { create(:project_wiki) } let(:wiki_link) { helper.wiki_sort_controls(wiki, direction) } - let(:classes) { "gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort" } + let(:classes) { "gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort" } def expected_link(direction, icon_class) path = "/#{wiki.project.full_path}/-/wikis/pages?direction=#{direction}" diff --git a/spec/requests/web_ide/remote_ide_controller_spec.rb b/spec/requests/web_ide/remote_ide_controller_spec.rb new file mode 100644 index 00000000000..9b99da3469c --- /dev/null +++ b/spec/requests/web_ide/remote_ide_controller_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_development do + using RSpec::Parameterized::TableSyntax + + let_it_be(:user) { create(:user) } + + let_it_be(:top_nav_partial) { 'layouts/header/_default' } + + let_it_be(:connection_token) { 'random1Connection3Token7' } + let_it_be(:remote_path) { 'test/foo/README.md' } + let_it_be(:return_url) { 'https://example.com/-/original/location' } + let_it_be(:csp_nonce) { 'just=some=noncense' } + + let(:remote_host) { 'my-remote-host.example.com:1234' } + let(:ff_vscode_web_ide) { true } + + before do + sign_in(user) + + stub_feature_flags(vscode_web_ide: ff_vscode_web_ide) + + allow_next_instance_of(described_class) do |instance| + allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce) + end + end + + shared_examples_for '404 response' do + it 'has not_found status' do + post_to_remote_ide + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "#index" do + context "when feature flag is on *and* user is not using legacy Web IDE" do + before do + post_to_remote_ide + end + + it "renders the correct layout" do + expect(response).to render_template(layout: 'fullscreen') + end + + it "renders with minimal: true" do + # This indirectly tests that `minimal: true` was passed to the fullscreen layout + expect(response).not_to render_template(top_nav_partial) + end + + it "renders root element with data" do + expected = { + connection_token: connection_token, + remote_host: remote_host, + remote_path: remote_path, + return_url: return_url, + csp_nonce: csp_nonce + } + + expect(find_root_element_data).to eq(expected) + end + + it "updates the content security policy with the correct connect sources" do + expect(find_csp_connect_src).to include( + "ws://#{remote_host}", + "wss://#{remote_host}", + "http://#{remote_host}", + "https://#{remote_host}" + ) + end + end + + context 'when remote_host does not have port' do + let(:remote_host) { "my-remote-host.example.com" } + + before do + post_to_remote_ide + end + + it "updates the content security policy with the correct remote_host" do + expect(find_csp_connect_src).to include( + "ws://#{remote_host}", + "wss://#{remote_host}", + "http://#{remote_host}", + "https://#{remote_host}" + ) + end + + it 'renders remote_host in root element data' do + expect(find_root_element_data).to include(remote_host: remote_host) + end + end + + context 'when feature flag is off' do + let(:ff_vscode_web_ide) { false } + + it_behaves_like '404 response' + end + + context "when the remote host is invalid" do + let(:remote_host) { 'invalid:host:1:1:' } + + it_behaves_like '404 response' + end + end + + def find_root_element_data + ide_attrs = Nokogiri::HTML.parse(response.body).at_css('#ide').attributes.transform_values(&:value) + + { + connection_token: ide_attrs['data-connection-token'], + remote_host: ide_attrs['data-remote-host'], + remote_path: ide_attrs['data-remote-path'], + return_url: ide_attrs['data-return-url'], + csp_nonce: ide_attrs['data-csp-nonce'] + } + end + + def find_csp_connect_src + csp = response.headers['Content-Security-Policy'] + + # Transform "default-src foo bar; connect-src foo bar; script-src ..." + # into array of connect-src values + csp.split(';') + .map(&:strip) + .find { |entry| entry.starts_with?('connect-src') } + .split(' ') + .drop(1) + end + + def post_to_remote_ide + params = { + connection_token: connection_token, + return_url: return_url + } + + post ide_remote_path(remote_host: remote_host, remote_path: remote_path), params: params + end +end diff --git a/spec/routing/web_ide_routing_spec.rb b/spec/routing/web_ide_routing_spec.rb new file mode 100644 index 00000000000..58c24189dfd --- /dev/null +++ b/spec/routing/web_ide_routing_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Web IDE routing", feature_category: :remote_development do + describe 'remote' do + it "routes to #index, without remote_path" do + expect(post("/-/ide/remote/my.env.gitlab.example.com%3A3443")).to route_to( + "web_ide/remote_ide#index", + remote_host: 'my.env.gitlab.example.com:3443' + ) + end + + it "routes to #index, with remote_path" do + expect(post("/-/ide/remote/my.env.gitlab.example.com%3A3443/foo/bar.dev/test.dir")).to route_to( + "web_ide/remote_ide#index", + remote_host: 'my.env.gitlab.example.com:3443', + remote_path: 'foo/bar.dev/test.dir' + ) + end + end +end diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index 33945509675..0368fd63357 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -4,6 +4,7 @@ RSpec.shared_examples "redis_shared_examples" do include StubENV let(:test_redis_url) { "redis://redishost:#{redis_port}" } + let(:test_cluster_config) { { cluster: [{ host: "redis://redishost", port: redis_port }] } } let(:config_file_name) { instance_specific_config_file } let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" } let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" } @@ -11,6 +12,7 @@ RSpec.shared_examples "redis_shared_examples" do let(:new_socket_path) { "/path/to/redis.sock" } let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" } let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" } + let(:config_cluster_format_host) { "spec/fixtures/config/redis_cluster_format_host.yml" } let(:redis_port) { 6379 } let(:redis_database) { 99 } let(:sentinel_port) { 26379 } @@ -191,6 +193,30 @@ RSpec.shared_examples "redis_shared_examples" do end end end + + context 'with redis cluster format' do + let(:config_file_name) { config_cluster_format_host } + + where(:rails_env, :host) do + [ + %w[development development-master], + %w[test test-master], + %w[production production-master] + ] + end + + with_them do + it 'returns hash with cluster and password' do + is_expected.to include(password: 'myclusterpassword', + cluster: [ + { host: "#{host}1", port: redis_port }, + { host: "#{host}2", port: redis_port } + ] + ) + is_expected.not_to have_key(:url) + end + end + end end end @@ -317,6 +343,14 @@ RSpec.shared_examples "redis_shared_examples" do expect(subject).to eq(redis_database) end end + + context 'with cluster-mode' do + let(:config_file_name) { config_cluster_format_host } + + it 'returns the correct db' do + expect(subject).to eq(0) + end + end end describe '#sentinels' do @@ -350,6 +384,14 @@ RSpec.shared_examples "redis_shared_examples" do is_expected.to be_nil end end + + context 'when cluster is defined' do + let(:config_file_name) { config_cluster_format_host } + + it 'returns nil' do + is_expected.to be_nil + end + end end describe '#sentinels?' do @@ -370,6 +412,14 @@ RSpec.shared_examples "redis_shared_examples" do is_expected.to be_falsey end end + + context 'when cluster is defined' do + let(:config_file_name) { config_cluster_format_host } + + it 'returns false' do + is_expected.to be_falsey + end + end end describe '#raw_config_hash' do @@ -377,6 +427,11 @@ RSpec.shared_examples "redis_shared_examples" do expect(subject).to receive(:fetch_config) { test_redis_url } expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url) end + + it 'returns cluster config without url key in a hash' do + expect(subject).to receive(:fetch_config) { test_cluster_config } + expect(subject.send(:raw_config_hash)).to eq(test_cluster_config) + end end describe '#fetch_config' do |