diff options
22 files changed, 280 insertions, 41 deletions
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 150378f7a7d..5bc6ce105b9 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,8 +1,10 @@ <script> import { GlAlert } from '@gitlab/ui'; +import { breakpoints } from '@gitlab/ui/dist/utils'; import { sortBy, throttle } from 'lodash'; import Draggable from 'vuedraggable'; import { mapState, mapGetters, mapActions } from 'vuex'; +import { contentTop } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { formatBoardLists } from 'ee_else_ce/boards/boards_util'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; @@ -142,7 +144,11 @@ export default { el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' }); }, setBoardHeight() { - this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`; + if (window.innerWidth < breakpoints.md) { + this.boardHeight = `${window.innerHeight - contentTop()}px`; + } else { + this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`; + } }, }, }; diff --git a/app/finders/environments/environments_finder.rb b/app/finders/environments/environments_finder.rb index f2dcba04349..011a317a349 100644 --- a/app/finders/environments/environments_finder.rb +++ b/app/finders/environments/environments_finder.rb @@ -41,7 +41,13 @@ module Environments def by_search(environments) if params[:search].present? - environments.for_name_like(params[:search], limit: nil) + if Feature.enabled?(:enable_environments_search_within_folder, project) + Environment.from_union( + environments.for_name_like(params[:search], limit: nil), + environments.for_name_like_within_folder(params[:search], limit: nil)) + else + environments.for_name_like(params[:search], limit: nil) + end else environments end diff --git a/app/models/environment.rb b/app/models/environment.rb index 2d3f342953f..4e2791c96c2 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,6 +6,7 @@ class Environment < ApplicationRecord include FastDestroyAll::Helpers include Presentable include NullifyIfBlank + include FromUnion self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 55.seconds @@ -96,7 +97,16 @@ class Environment < ApplicationRecord # Search environments which have names like the given query. # Do not set a large limit unless you've confirmed that it works on gitlab.com scale. scope :for_name_like, -> (query, limit: 5) do - where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit) + top_level = 'LOWER(environments.name) LIKE LOWER(?) || \'%\'' + + where(top_level, sanitize_sql_like(query)).limit(limit) + end + + scope :for_name_like_within_folder, -> (query, limit: 5) do + within_folder = 'LOWER(ltrim(environments.name, environments.environment_type'\ + ' || \'/\')) LIKE LOWER(?) || \'%\'' + + where(within_folder, sanitize_sql_like(query)).limit(limit) end scope :for_project, -> (project) { where(project_id: project) } diff --git a/config/feature_flags/development/enable_environments_search_within_folder.yml b/config/feature_flags/development/enable_environments_search_within_folder.yml new file mode 100644 index 00000000000..91fe42503d8 --- /dev/null +++ b/config/feature_flags/development/enable_environments_search_within_folder.yml @@ -0,0 +1,8 @@ +--- +name: enable_environments_search_within_folder +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102227/diffs +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382108 +milestone: '15.6' +type: development +group: group::release +default_enabled: false diff --git a/db/migrate/20221102150737_index_environments_for_name_search_within_folder.rb b/db/migrate/20221102150737_index_environments_for_name_search_within_folder.rb new file mode 100644 index 00000000000..10fd8b33c45 --- /dev/null +++ b/db/migrate/20221102150737_index_environments_for_name_search_within_folder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class IndexEnvironmentsForNameSearchWithinFolder < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_environments_for_name_search_within_folder' + + def up + add_concurrent_index :environments, + "project_id, lower(ltrim(name, environment_type || '/')) varchar_pattern_ops, state", name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :environments, INDEX_NAME + end +end diff --git a/db/schema_migrations/20221102150737 b/db/schema_migrations/20221102150737 new file mode 100644 index 00000000000..83e3c1e77ed --- /dev/null +++ b/db/schema_migrations/20221102150737 @@ -0,0 +1 @@ +600e0c6bd79850846c38de38f175889cee731b5619dfbd084e1bd4438d13d387
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 722b3a3fa08..92dbe722bf9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -28967,6 +28967,8 @@ CREATE INDEX index_emails_on_user_id ON emails USING btree (user_id); CREATE INDEX index_enabled_clusters_on_id ON clusters USING btree (id) WHERE (enabled = true); +CREATE INDEX index_environments_for_name_search_within_folder ON environments USING btree (project_id, lower(ltrim((name)::text, ((environment_type)::text || '/'::text))) varchar_pattern_ops, state); + CREATE INDEX index_environments_on_merge_request_id ON environments USING btree (merge_request_id); CREATE INDEX index_environments_on_name_varchar_pattern_ops ON environments USING btree (name varchar_pattern_ops); diff --git a/doc/administration/gitaly/configure_gitaly.md b/doc/administration/gitaly/configure_gitaly.md index e7a6fc35ced..17550550062 100644 --- a/doc/administration/gitaly/configure_gitaly.md +++ b/doc/administration/gitaly/configure_gitaly.md @@ -49,6 +49,9 @@ NOTE: When configured to run on their own servers, Gitaly servers must be [upgraded](../../update/package/index.md) before Gitaly clients in your cluster. +NOTE: +[Disk requirements](index.md#disk-requirements) apply to Gitaly nodes. + The process for setting up Gitaly on its own server is: 1. [Install Gitaly](#install-gitaly). diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 1b7e1f0f7c6..6e2b334a6b4 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -90,6 +90,50 @@ If you are unable to use either method, contact customer support for restoration Contact customer support for immediate help in restoration or recovery. +## Disk requirements + +Gitaly and Gitaly Cluster require fast local storage to perform effectively because they are heavy I/O-based processes. Therefore, +we strongly recommend that all Gitaly nodes use solid-state drives (SSDs). + +These SSDs should have a throughput of at least: + +- 8,000 input/output operations per second (IOPS) for read operations. +- 2,000 IOPS for write operations. + +These IOPS values are initial recommendations, and may be adjusted to greater or lesser values +depending on the scale of your environment's workload. If you’re running the environment on a +cloud provider, refer to their documentation about how to configure IOPS correctly. + +For repository data, only local storage is supported for Gitaly and Gitaly Cluster for performance and consistency reasons. Alternatives such as +[NFS](#moving-beyond-nfs) or [cloud-based systems](../nfs.md#avoid-using-cloud-based-file-systems) are not supported. + +### Moving beyond NFS + +Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be unavailable starting +November 22, 2022. See our [statement of support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) +for more details. + +[Network File System (NFS)](https://en.wikipedia.org/wiki/Network_File_System) +is not well suited to Git workloads which are CPU and IOPS sensitive. +Specifically: + +- Git is sensitive to file system latency. Some operations require many + read operations. Operations that are fast on block storage can become an order of + magnitude slower. This significantly impacts GitLab application performance. +- NFS performance optimizations that prevent the performance gap between + block storage and NFS being even wider are vulnerable to race conditions. We have observed + [data inconsistencies](https://gitlab.com/gitlab-org/gitaly/-/issues/2589) + in production environments caused by simultaneous writes to different NFS + clients. Data corruption is not an acceptable risk. + +Gitaly Cluster is purpose built to provide reliable, high performance, fault +tolerant Git storage. + +Further reading: + +- Blog post: [The road to Gitaly v1.0 (aka, why GitLab doesn't require NFS for storing Git data anymore)](https://about.gitlab.com/blog/2018/09/12/the-road-to-gitaly-1-0/) +- Blog post: [How we spent two weeks hunting an NFS bug in the Linux kernel](https://about.gitlab.com/blog/2018/11/14/how-we-spent-two-weeks-hunting-an-nfs-bug/) + ## Directly accessing repositories GitLab doesn't advise directly accessing Gitaly repositories stored on disk with a Git client or any other tool, @@ -405,33 +449,6 @@ The leftover state is eventually cleaned up. Unlike Gitaly, Gitaly Cluster doesn't move the repositories in the storages but only virtually moves the repository by updating the relative path of the repository in the metadata store. -### Moving beyond NFS - -Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be unavailable starting -November 22, 2022. See our [statement of support](https://about.gitlab.com/support/statement-of-support/#gitaly-and-nfs) -for more details. - -[Network File System (NFS)](https://en.wikipedia.org/wiki/Network_File_System) -is not well suited to Git workloads which are CPU and IOPS sensitive. -Specifically: - -- Git is sensitive to file system latency. Some operations require many - read operations. Operations that are fast on block storage can become an order of - magnitude slower. This significantly impacts GitLab application performance. -- NFS performance optimizations that prevent the performance gap between - block storage and NFS being even wider are vulnerable to race conditions. We have observed - [data inconsistencies](https://gitlab.com/gitlab-org/gitaly/-/issues/2589) - in production environments caused by simultaneous writes to different NFS - clients. Data corruption is not an acceptable risk. - -Gitaly Cluster is purpose built to provide reliable, high performance, fault -tolerant Git storage. - -Further reading: - -- Blog post: [The road to Gitaly v1.0 (aka, why GitLab doesn't require NFS for storing Git data anymore)](https://about.gitlab.com/blog/2018/09/12/the-road-to-gitaly-1-0/) -- Blog post: [How we spent two weeks hunting an NFS bug in the Linux kernel](https://about.gitlab.com/blog/2018/11/14/how-we-spent-two-weeks-hunting-an-nfs-bug/) - ### Components Gitaly Cluster consists of multiple components: diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 8cf32a6aaa3..19ce24a6064 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -28,6 +28,9 @@ The minimum recommended configuration for a Gitaly Cluster requires: - 3 Praefect nodes - 3 Gitaly nodes (1 primary, 2 secondary) +NOTE: +[Disk requirements](index.md#disk-requirements) apply to Gitaly nodes. + You should configure an odd number of Gitaly nodes so that transactions have a tie-breaker in case one of the Gitaly nodes fails in a mutating RPC call. diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index f1ca8afa62c..f75c084dd83 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -188,6 +188,31 @@ In this example: - `DEPLOY_ENVIRONMENT` is listed in the **Run pipeline** page, but with no value set. The user is expected to define the value each time the pipeline is run manually. +##### Configure a list of selectable values for a prefilled variable + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, +ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. +The feature is not ready for production use. + +You can define an array of CI/CD variable values the user can select from when running a pipeline manually. +These values are in a dropdown list in the **Run pipeline** page. The first value +in the array is the value selected by default. + +For example: + +```yaml +variables: + DEPLOY_ENVIRONMENT: + value: + - "production" + - "staging" + - "canary" + description: "The deployment target. Set to 'production' by default." +``` + ### Run a pipeline by using a URL query string > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24146) in GitLab 12.5. diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 3392304775a..3f5e11129f9 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -4228,7 +4228,8 @@ deploy_review_job: #### `variables:description` -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5, `variables:value` can contain an array of values. Use the `description` keyword to define a [pipeline-level (global) variable that is prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines) when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually). @@ -4240,6 +4241,7 @@ If used with `value`, the variable value is also prefilled when running a pipeli **Possible inputs**: - A string. +- An array of strings. **Example of `variables:description`**: @@ -4254,6 +4256,7 @@ variables: - A global variable defined with `value` but no `description` behaves the same as [`variables`](#variables). +- `variables:value` can [contain an array of selectable values](../pipelines/index.md#configure-a-list-of-selectable-values-for-a-prefilled-variable). #### `variables:expand` diff --git a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml index d933007ec61..89944e347f6 100644 --- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.gitlab-ci.yml @@ -16,7 +16,7 @@ variables: COVFUZZ_VERSION: v3 # This is for users who have an offline environment and will have to replicate gitlab-cov-fuzz release binaries # to their own servers - COVFUZZ_URL_PREFIX: "https://gitlab.com/gitlab-org/security-products/analyzers/gitlab-cov-fuzz/-/raw" + COVFUZZ_URL_PREFIX: "https://gitlab.com/security-products/gitlab-cov-fuzz/-/raw" coverage_fuzzing_unlicensed: diff --git a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.latest.gitlab-ci.yml index feed4c47157..4f6ba427058 100644 --- a/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Coverage-Fuzzing.latest.gitlab-ci.yml @@ -16,7 +16,7 @@ variables: COVFUZZ_VERSION: v3 # This is for users who have an offline environment and will have to replicate gitlab-cov-fuzz release binaries # to their own servers - COVFUZZ_URL_PREFIX: "https://gitlab.com/gitlab-org/security-products/analyzers/gitlab-cov-fuzz/-/raw" + COVFUZZ_URL_PREFIX: "https://gitlab.com/security-products/gitlab-cov-fuzz/-/raw" coverage_fuzzing_unlicensed: diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index 92ffa65fe74..44e53e9ec70 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -161,7 +161,7 @@ module Gitlab def validate_url! return if Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? - Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false) + Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false, schemes: %w[http https]) end def service_account_exists?(resource) diff --git a/lib/gitlab/utils/delegator_override/validator.rb b/lib/gitlab/utils/delegator_override/validator.rb index 4449fa75877..7fa5cc4deef 100644 --- a/lib/gitlab/utils/delegator_override/validator.rb +++ b/lib/gitlab/utils/delegator_override/validator.rb @@ -67,7 +67,7 @@ module Gitlab (delegator_class.instance_methods - allowlist).each do |method_name| target_classes.each do |target_class| - next unless target_class.instance_methods.include?(method_name) + next unless target_class.method_defined?(method_name) errors << generate_error(method_name, target_class, delegator_class) end diff --git a/lib/gitlab/utils/override.rb b/lib/gitlab/utils/override.rb index 39670a835a6..f83ebba7c3f 100644 --- a/lib/gitlab/utils/override.rb +++ b/lib/gitlab/utils/override.rb @@ -67,8 +67,8 @@ module Gitlab private def instance_method_defined?(klass, name) - klass.instance_methods(false).include?(name) || - klass.private_instance_methods(false).include?(name) + klass.method_defined?(name, false) || + klass.private_method_defined?(name, false) end def find_direct_method(klass, name) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 5927f20df97..2334521b8a8 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -91,6 +91,34 @@ RSpec.describe Projects::EnvironmentsController do expect(json_response['stopped_count']).to eq 1 end + it 'supports search within environment folder name' do + create(:environment, project: project, name: 'review-app', state: :available) + + get :index, params: environment_params(format: :json, search: 'review') + + expect(environments.map { |env| env['name'] }).to contain_exactly('review-app', + 'staging/review-1', + 'staging/review-2') + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end + + context 'when enable_environments_search_within_folder FF is disabled' do + before do + stub_feature_flags(enable_environments_search_within_folder: false) + end + + it 'ignores name inside folder' do + create(:environment, project: project, name: 'review-app', state: :available) + + get :index, params: environment_params(format: :json, search: 'review') + + expect(environments.map { |env| env['name'] }).to contain_exactly('review-app') + expect(json_response['available_count']).to eq 1 + expect(json_response['stopped_count']).to eq 0 + end + end + it 'sets the polling interval header' do subject diff --git a/spec/finders/environments/environments_finder_spec.rb b/spec/finders/environments/environments_finder_spec.rb index 04fbd4067b4..df66bbdc235 100644 --- a/spec/finders/environments/environments_finder_spec.rb +++ b/spec/finders/environments/environments_finder_spec.rb @@ -51,15 +51,35 @@ RSpec.describe Environments::EnvironmentsFinder do end context 'with search and states' do + let_it_be(:environment_available_b) { create(:environment, :available, name: 'test/foldered-env', project: project) } + it 'searches environments by name and state' do result = described_class.new(project, user, search: 'test', states: :available).execute - expect(result).to contain_exactly(environment_available) + expect(result).to contain_exactly(environment_available, environment_available_b) + end + + it 'searches environments by name inside folder and state' do + result = described_class.new(project, user, search: 'folder', states: :available).execute + + expect(result).to contain_exactly(environment_available_b) + end + + context 'when enable_environments_search_within_folder FF is disabled' do + before do + stub_feature_flags(enable_environments_search_within_folder: false) + end + + it 'ignores name inside folder' do + result = described_class.new(project, user, search: 'folder', states: :available).execute + + expect(result).to be_empty + end end end context 'with id' do - it 'searches environments by name and state' do + it 'searches environments by name and id' do result = described_class.new(project, user, search: 'test', environment_ids: [environment_available.id]).execute expect(result).to contain_exactly(environment_available) diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index b2138700602..39b2fe2ebbf 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -123,15 +123,32 @@ describe('BoardContent', () => { expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); - it('resizes the list on resize', async () => { + it('on small screens, sets board container height to full height', async () => { window.innerHeight = 1000; + window.innerWidth = 767; jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 }); wrapper.vm.resizeObserver.trigger(); await nextTick(); - expect(wrapper.findComponent({ ref: 'list' }).attributes('style')).toBe('height: 900px;'); + const style = wrapper.findComponent({ ref: 'list' }).attributes('style'); + + expect(style).toBe('height: 1000px;'); + }); + + it('on large screens, sets board container height fill area below filters', async () => { + window.innerHeight = 1000; + window.innerWidth = 768; + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 }); + + wrapper.vm.resizeObserver.trigger(); + + await nextTick(); + + const style = wrapper.findComponent({ ref: 'list' }).attributes('style'); + + expect(style).toBe('height: 900px;'); }); }); diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 8abd041fd4e..bc116e6a20d 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -132,6 +132,14 @@ RSpec.describe Gitlab::Kubernetes::KubeClient do it_behaves_like 'local address' end + context 'when a non HTTP/HTTPS URL is provided' do + let(:api_url) { 'ssh://192.168.1.2' } + + it 'raises an error' do + expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError) + end + end + it 'falls back to default options, but allows overriding' do client = described_class.new(api_url) defaults = Gitlab::Kubernetes::KubeClient::DEFAULT_KUBECLIENT_OPTIONS diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 8a3d43f58e0..d4694109b12 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -282,6 +282,72 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end end + describe '.for_name_like_within_folder' do + subject { project.environments.for_name_like_within_folder(query, limit: limit) } + + let!(:environment) { create(:environment, name: 'review/test-app', project: project) } + let!(:environment_a) { create(:environment, name: 'test-app', project: project) } + let(:query) { 'test' } + let(:limit) { 5 } + + it 'returns a found name' do + is_expected.to contain_exactly(environment) + end + + it 'does not return environment without folder' do + is_expected.not_to include(environment_a) + end + + context 'when query is test-app' do + let(:query) { 'test-app' } + + it 'returns a found name' do + is_expected.to include(environment) + end + end + + context 'when query is test-app-a' do + let(:query) { 'test-app-a' } + + it 'returns empty array' do + is_expected.to be_empty + end + end + + context 'when query is empty string' do + let(:query) { '' } + let!(:environment_b) { create(:environment, name: 'review/test-app-1', project: project) } + + it 'returns only the foldered environments' do + is_expected.to contain_exactly(environment, environment_b) + end + end + + context 'when query is nil' do + let(:query) {} + + it 'raises an error' do + expect { subject }.to raise_error(NoMethodError) + end + end + + context 'when query is partially matched in the middle of environment name' do + let(:query) { 'app' } + + it 'returns empty array' do + is_expected.to be_empty + end + end + + context 'when query contains a wildcard character' do + let(:query) { 'test%' } + + it 'prevents wildcard injection' do + is_expected.to be_empty + end + end + end + describe '.auto_stoppable' do subject { described_class.auto_stoppable(limit) } |