Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js41
-rw-r--r--app/assets/javascripts/ide/remote/index.js40
-rw-r--r--app/assets/javascripts/lib/utils/create_and_submit_form.js26
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue2
-rw-r--r--app/assets/javascripts/pages/web_ide/remote_ide/index.js3
-rw-r--r--app/controllers/web_ide/remote_ide_controller.rb51
-rw-r--r--app/helpers/ide_helper.rb4
-rw-r--r--app/helpers/nav/top_nav_helper.rb13
-rw-r--r--app/helpers/sorting_helper.rb2
-rw-r--r--app/helpers/wiki_helper.rb2
-rw-r--r--app/views/web_ide/remote_ide/index.html.haml5
-rw-r--r--config/routes.rb6
-rw-r--r--db/post_migrate/20221206173132_add_issues_work_item_type_id_index.rb15
-rw-r--r--db/schema_migrations/202212061731321
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/instance_limits.md2
-rw-r--r--lib/gitlab/redis/wrapper.rb15
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/qa/page/main/login.rb4
-rw-r--r--qa/qa/page/main/menu.rb2
-rw-r--r--spec/db/schema_spec.rb4
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb8
-rw-r--r--spec/fixtures/config/redis_cluster_format_host.yml29
-rw-r--r--spec/frontend/__helpers__/mock_window_location_helper.js9
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js66
-rw-r--r--spec/frontend/ide/remote/index_spec.js91
-rw-r--r--spec/frontend/lib/utils/create_and_submit_form_spec.js73
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js3
-rw-r--r--spec/helpers/ide_helper_spec.rb5
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb24
-rw-r--r--spec/helpers/wiki_helper_spec.rb2
-rw-r--r--spec/requests/web_ide/remote_ide_controller_spec.rb141
-rw-r--r--spec/routing/web_ide_routing_spec.rb22
-rw-r--r--spec/support/redis/redis_shared_examples.rb55
34 files changed, 730 insertions, 47 deletions
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index edc93fdd184..5d50d2eec17 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -1,10 +1,28 @@
import { start } from '@gitlab/web-ide';
+import { __ } from '~/locale';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
+const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
+ const remotePath = cleanLeadingSeparator(remotePathArg);
+
+ const replacers = {
+ ':remote_host': encodeURIComponent(remoteHost),
+ ':remote_path': encodeURIComponent(remotePath).replaceAll('%2F', '/'),
+ };
+
+ // why: Use the function callback of "replace" so we replace both keys at once
+ return ideRemotePath.replace(/(:remote_host|:remote_path)/g, (key) => {
+ return replacers[key];
+ });
+};
+
export const initGitlabWebIDE = async (el) => {
// what: Pull what we need from the element. We will replace it soon.
- const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
+ const { cspNonce: nonce, branchName: ref, projectPath, ideRemotePath } = el.dataset;
const rootEl = setupRootElement(el);
@@ -13,5 +31,26 @@ export const initGitlabWebIDE = async (el) => {
nonce,
projectPath,
ref,
+ async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
+ const confirmed = await confirmAction(
+ __('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
+ {
+ primaryBtnText: __('Start remote connection'),
+ cancelBtnText: __('Continue editing'),
+ },
+ );
+
+ if (!confirmed) {
+ return;
+ }
+
+ createAndSubmitForm({
+ url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
+ data: {
+ connection_token: connectionToken,
+ return_url: window.location.href,
+ },
+ });
+ },
});
};
diff --git a/app/assets/javascripts/ide/remote/index.js b/app/assets/javascripts/ide/remote/index.js
new file mode 100644
index 00000000000..fb8db20c0c1
--- /dev/null
+++ b/app/assets/javascripts/ide/remote/index.js
@@ -0,0 +1,40 @@
+import { startRemote } from '@gitlab/web-ide';
+import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide';
+import { isSameOriginUrl, joinPaths } from '~/lib/utils/url_utility';
+
+/**
+ * @param {Element} rootEl
+ */
+export const mountRemoteIDE = async (el) => {
+ const {
+ remoteHost: remoteAuthority,
+ remotePath: hostPath,
+ cspNonce,
+ connectionToken,
+ returnUrl,
+ } = el.dataset;
+
+ const rootEl = setupRootElement(el);
+
+ const visitReturnUrl = () => {
+ // security: Only change `href` if of the same origin as current page
+ if (returnUrl && isSameOriginUrl(returnUrl)) {
+ window.location.href = returnUrl;
+ } else {
+ window.location.reload();
+ }
+ };
+
+ startRemote(rootEl, {
+ ...getBaseConfig(),
+ nonce: cspNonce,
+ connectionToken,
+ // remoteAuthority must start with "/"
+ remoteAuthority: joinPaths('/', remoteAuthority),
+ // hostPath must start with "/"
+ hostPath: joinPaths('/', hostPath),
+ // TODO Handle error better
+ handleError: visitReturnUrl,
+ handleClose: visitReturnUrl,
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/create_and_submit_form.js b/app/assets/javascripts/lib/utils/create_and_submit_form.js
new file mode 100644
index 00000000000..fce4f898f2f
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/create_and_submit_form.js
@@ -0,0 +1,26 @@
+import csrf from '~/lib/utils/csrf';
+
+export const createAndSubmitForm = ({ url, data }) => {
+ const form = document.createElement('form');
+
+ form.action = url;
+ // For now we only support 'post'.
+ // `form.method` doesn't support other methods so we would need to
+ // use a hidden `_method` input, which is out of scope for now.
+ form.method = 'post';
+ form.style.display = 'none';
+
+ Object.entries(data)
+ .concat([['authenticity_token', csrf.token]])
+ .forEach(([key, value]) => {
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = key;
+ input.value = value;
+
+ form.appendChild(input);
+ });
+
+ document.body.appendChild(form);
+ form.submit();
+};
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 6fb001e5e92..0a94f67ea5e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
@@ -47,7 +47,7 @@ export default {
</script>
<template>
- <div data-qa-selector="package-path" class="gl-display-flex gl-align-items-center">
+ <div data-qa-selector="package_path" class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
<gl-link
diff --git a/app/assets/javascripts/pages/web_ide/remote_ide/index.js b/app/assets/javascripts/pages/web_ide/remote_ide/index.js
new file mode 100644
index 00000000000..463798e85b9
--- /dev/null
+++ b/app/assets/javascripts/pages/web_ide/remote_ide/index.js
@@ -0,0 +1,3 @@
+import { mountRemoteIDE } from '~/ide/remote';
+
+mountRemoteIDE(document.getElementById('ide'));
diff --git a/app/controllers/web_ide/remote_ide_controller.rb b/app/controllers/web_ide/remote_ide_controller.rb
new file mode 100644
index 00000000000..fd867f4cd9b
--- /dev/null
+++ b/app/controllers/web_ide/remote_ide_controller.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'uri'
+
+module WebIde
+ class RemoteIdeController < ApplicationController
+ rescue_from URI::InvalidComponentError, with: :render_404
+
+ before_action :allow_remote_ide_content_security_policy
+
+ feature_category :remote_development
+
+ urgency :low
+
+ def index
+ return render_404 unless Feature.enabled?(:vscode_web_ide, current_user)
+
+ render layout: 'fullscreen', locals: { minimal: true, data: root_element_data }
+ end
+
+ private
+
+ def allow_remote_ide_content_security_policy
+ return if request.content_security_policy.directives.blank?
+
+ default_src = Array(request.content_security_policy.directives['default-src'] || [])
+
+ request.content_security_policy.directives['connect-src'] ||= default_src
+ request.content_security_policy.directives['connect-src'].concat(connect_src_urls)
+ end
+
+ def connect_src_urls
+ # It's okay if "port" is null
+ host, port = params.require(:remote_host).split(':')
+
+ # This could throw URI::InvalidComponentError. We go ahead and let it throw
+ # and let the controller recover with a bad_request response
+ %w[ws wss http https].map { |scheme| URI::Generic.build(scheme: scheme, host: host, port: port).to_s }
+ end
+
+ def root_element_data
+ {
+ connection_token: params.fetch(:connection_token, ''),
+ remote_host: params.require(:remote_host),
+ remote_path: params.fetch(:remote_path, ''),
+ return_url: params.fetch(:return_url, ''),
+ csp_nonce: content_security_policy_nonce
+ }
+ end
+ end
+end
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 34f4749c42a..aa2c8ad3719 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -24,7 +24,9 @@ module IdeHelper
def new_ide_data
{
'project-path' => @project&.path_with_namespace,
- 'csp-nonce' => content_security_policy_nonce
+ 'csp-nonce' => content_security_policy_nonce,
+ # We will replace these placeholders in the FE
+ 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path')
}
end
diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb
index 751900f4593..bd4d661ab49 100644
--- a/app/helpers/nav/top_nav_helper.rb
+++ b/app/helpers/nav/top_nav_helper.rb
@@ -101,8 +101,7 @@ module Nav
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:switch_to],
active: nav == 'project' || active_nav_link?(path: %w[root#index projects#trending projects#starred dashboard/projects#index]),
- css_class: 'qa-projects-dropdown',
- data: { track_label: "projects_dropdown", track_action: "click_dropdown" },
+ data: { track_label: "projects_dropdown", track_action: "click_dropdown", qa_selector: "projects_dropdown" },
view: PROJECTS_VIEW,
shortcut_href: dashboard_projects_path,
**projects_menu_item_attrs
@@ -116,8 +115,7 @@ module Nav
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:switch_to],
active: nav == 'group' || active_nav_link?(path: %w[dashboard/groups explore/groups]),
- css_class: 'qa-groups-dropdown',
- data: { track_label: "groups_dropdown", track_action: "click_dropdown" },
+ data: { track_label: "groups_dropdown", track_action: "click_dropdown", qa_selector: "groups_dropdown" },
view: GROUPS_VIEW,
shortcut_href: dashboard_groups_path,
**groups_menu_item_attrs
@@ -133,7 +131,7 @@ module Nav
href: dashboard_milestones_path,
active: active_nav_link?(controller: 'dashboard/milestones'),
icon: 'clock',
- data: { qa_selector: 'milestones_link', **menu_data_tracking_attrs('milestones') },
+ data: { **menu_data_tracking_attrs('milestones') },
shortcut_class: 'dashboard-shortcuts-milestones'
)
end
@@ -156,7 +154,7 @@ module Nav
href: activity_dashboard_path,
active: active_nav_link?(path: 'dashboard#activity'),
icon: 'history',
- data: { qa_selector: 'activity_link', **menu_data_tracking_attrs('activity') },
+ data: { **menu_data_tracking_attrs('activity') },
shortcut_class: 'dashboard-shortcuts-activity'
)
end
@@ -173,9 +171,8 @@ module Nav
title: title,
active: active_nav_link?(controller: 'admin/dashboard'),
icon: 'admin',
- css_class: 'qa-admin-area-link',
href: admin_root_path,
- data: { qa_selector: 'menu_item_link', qa_title: title, **menu_data_tracking_attrs(title) }
+ data: { qa_selector: 'admin_area_link', **menu_data_tracking_attrs(title) }
)
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 37aaea50a43..3a4465b1a4c 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -301,7 +301,7 @@ module SortingHelper
end
def sort_direction_button(reverse_url, reverse_sort, sort_value)
- link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort'
+ link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort'
icon = sort_direction_icon(sort_value)
url = reverse_url
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 017a1861905..b2b8ca2a120 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -60,7 +60,7 @@ module WikiHelper
end
def wiki_sort_controls(wiki, direction)
- link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort'
+ link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
title = direction == 'desc' ? _('Sort direction: Descending') : _('Sort direction: Ascending')
diff --git a/app/views/web_ide/remote_ide/index.html.haml b/app/views/web_ide/remote_ide/index.html.haml
new file mode 100644
index 00000000000..f007794d056
--- /dev/null
+++ b/app/views/web_ide/remote_ide/index.html.haml
@@ -0,0 +1,5 @@
+- data = local_assigns.fetch(:data)
+
+- page_title _('Web IDE')
+
+= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Connecting to the remote environment...') }
diff --git a/config/routes.rb b/config/routes.rb
index 3482b0566ce..a9cb462b326 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -155,6 +155,12 @@ InitializerConnections.with_disabled_database_connections do
get '/merge_requests/:merge_request_id', to: 'ide#index', constraints: { merge_request_id: /\d+/ }
get '/', to: 'ide#index'
end
+
+ # Remote host can contain "." characters so it needs a constraint
+ post 'remote/:remote_host(/*remote_path)',
+ as: :remote,
+ to: 'web_ide/remote_ide#index',
+ constraints: { remote_host: %r{[^/?]+} }
end
draw :operations
diff --git a/db/post_migrate/20221206173132_add_issues_work_item_type_id_index.rb b/db/post_migrate/20221206173132_add_issues_work_item_type_id_index.rb
new file mode 100644
index 00000000000..b50da0e4644
--- /dev/null
+++ b/db/post_migrate/20221206173132_add_issues_work_item_type_id_index.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddIssuesWorkItemTypeIdIndex < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_issues_on_work_item_type_id'
+
+ def up
+ add_concurrent_index :issues, :work_item_type_id, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :issues, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20221206173132 b/db/schema_migrations/20221206173132
new file mode 100644
index 00000000000..7f34421d270
--- /dev/null
+++ b/db/schema_migrations/20221206173132
@@ -0,0 +1 @@
+c2e7a2c25e281419e2e401e3bff661c706386900faffc784efcfbf7aca169ed8 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 0a99fc96493..d5d03f805c3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29641,6 +29641,8 @@ CREATE INDEX index_issues_on_updated_at ON issues USING btree (updated_at);
CREATE INDEX index_issues_on_updated_by_id ON issues USING btree (updated_by_id) WHERE (updated_by_id IS NOT NULL);
+CREATE INDEX index_issues_on_work_item_type_id ON issues USING btree (work_item_type_id);
+
CREATE INDEX index_iterations_cadences_on_group_id ON iterations_cadences USING btree (group_id);
CREATE UNIQUE INDEX index_jira_connect_installations_on_client_key ON jira_connect_installations USING btree (client_key);
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index a3b9df81a25..129daa95301 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -378,7 +378,7 @@ and to limit memory consumption.
When using offset-based pagination in the REST API, there is a limit to the maximum
requested offset into the set of results. This limit is only applied to endpoints that
-support keyset-based pagination. More information about pagination options can be
+also support keyset-based pagination. More information about pagination options can be
found in the [API documentation section on pagination](../api/index.md#pagination).
To set this limit for a self-managed installation, run the following in the
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index 75dbccb965d..0e5389dc995 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -144,11 +144,20 @@ module Gitlab
def redis_store_options
config = raw_config_hash
+ config[:instrumentation_class] ||= self.class.instrumentation_class
+
+ if config[:cluster].present?
+ config[:db] = 0 # Redis Cluster only supports db 0
+ config
+ else
+ parse_redis_url(config)
+ end
+ end
+
+ def parse_redis_url(config)
redis_url = config.delete(:url)
redis_uri = URI.parse(redis_url)
- config[:instrumentation_class] ||= self.class.instrumentation_class
-
if redis_uri.scheme == 'unix'
# Redis::Store does not handle Unix sockets well, so let's do it for them
config[:path] = redis_uri.path
@@ -178,7 +187,7 @@ module Gitlab
{ url: '' }
end
- if config_hash[:url].blank?
+ if config_hash[:url].blank? && config_hash[:cluster].blank?
config_hash[:url] = legacy_fallback_urls[self.class.store_name] || legacy_fallback_urls[self.class.config_fallback.store_name]
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 246d0bc0470..4e693b29647 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5227,6 +5227,9 @@ msgid_plural "Are you sure you want to import %d repositories?"
msgstr[0] ""
msgstr[1] ""
+msgid "Are you sure you want to leave the Web IDE? All unsaved changes will be lost."
+msgstr ""
+
msgid "Are you sure you want to lock %{path}?"
msgstr ""
@@ -10446,6 +10449,9 @@ msgstr ""
msgid "Connecting to terminal sync service"
msgstr ""
+msgid "Connecting to the remote environment..."
+msgstr ""
+
msgid "Connecting..."
msgstr ""
@@ -39417,6 +39423,9 @@ msgstr ""
msgid "Start merge train..."
msgstr ""
+msgid "Start remote connection"
+msgstr ""
+
msgid "Start search"
msgstr ""
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index d7ca8223862..8af78bb86c6 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -12,6 +12,10 @@ module QA
element :change_password_button
end
+ view 'app/views/devise/sessions/new.html.haml' do
+ element :register_link
+ end
+
view 'app/views/devise/sessions/_new_base.html.haml' do
element :login_field
element :password_field
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index ecd71e7c2f4..1e050d79e23 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -9,6 +9,7 @@ module QA
view 'app/views/layouts/header/_current_user_dropdown.html.haml' do
element :sign_out_link
element :edit_profile_link
+ element :user_profile_link
end
view 'app/views/layouts/header/_default.html.haml' do
@@ -39,6 +40,7 @@ module QA
element :projects_dropdown
element :groups_dropdown
element :snippets_link
+ element :menu_item_link
end
view 'app/views/layouts/_search.html.haml' do
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