diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js
index 5e8b013d480..53a55dcd6bd 100644
--- a/spec/frontend/vue_shared/components/identicon_spec.js
+++ b/spec/frontend/vue_shared/components/identicon_spec.js
@@ -4,12 +4,17 @@ import IdenticonComponent from '~/vue_shared/components/identicon.vue';
describe('Identicon', () => {
let wrapper;
- const createComponent = () => {
+ const defaultProps = {
+ entityId: 1,
+ entityName: 'entity-name',
+ sizeClass: 's40',
+ };
+
+ const createComponent = (props = {}) => {
wrapper = shallowMount(IdenticonComponent, {
propsData: {
- entityId: 1,
- entityName: 'entity-name',
- sizeClass: 's40',
+ ...defaultProps,
+ ...props,
},
});
};
@@ -19,15 +24,27 @@ describe('Identicon', () => {
wrapper = null;
});
- it('matches snapshot', () => {
- createComponent();
+ describe('entity id is a number', () => {
+ beforeEach(createComponent);
- expect(wrapper.element).toMatchSnapshot();
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('adds a correct class to identicon', () => {
+ expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
+ });
});
- it('adds a correct class to identicon', () => {
- createComponent();
+ describe('entity id is a GraphQL id', () => {
+ beforeEach(() => createComponent({ entityId: 'gid://gitlab/Project/8' }));
- expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('adds a correct class to identicon', () => {
+ expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
+ });
});
});
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index d679768be6c..05231cc6d09 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -71,6 +71,28 @@ describe ApplicationHelper do
end
end
+ describe '#admin_section?' do
+ context 'when controller is under the admin namespace' do
+ before do
+ allow(helper).to receive(:controller).and_return(Admin::UsersController.new)
+ end
+
+ it 'returns true' do
+ expect(helper.admin_section?).to eq(true)
+ end
+ end
+
+ context 'when controller is not under the admin namespace' do
+ before do
+ allow(helper).to receive(:controller).and_return(UsersController.new)
+ end
+
+ it 'returns true' do
+ expect(helper.admin_section?).to eq(false)
+ end
+ end
+ end
+
describe 'simple_sanitize' do
let(:a_tag) { '
Foo' }
diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
index f46f9d76a1e..8b0d4d786cd 100644
--- a/spec/lib/gitlab/auth/o_auth/provider_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
@@ -63,7 +63,7 @@ describe Gitlab::Auth::OAuth::Provider do
context 'for an OmniAuth provider' do
before do
provider = OpenStruct.new(
- name: 'google',
+ name: 'google_oauth2',
app_id: 'asd123',
app_secret: 'asd123'
)
@@ -71,8 +71,16 @@ describe Gitlab::Auth::OAuth::Provider do
end
context 'when the provider exists' do
+ subject { described_class.config_for('google_oauth2') }
+
it 'returns the config' do
- expect(described_class.config_for('google')).to be_a(OpenStruct)
+ expect(subject).to be_a(OpenStruct)
+ end
+
+ it 'merges defaults with the given configuration' do
+ defaults = Gitlab::OmniauthInitializer.default_arguments_for('google_oauth2').deep_stringify_keys
+
+ expect(subject['args']).to include(defaults)
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
index ea1fbc98c9f..0ad5dc189c0 100644
--- a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
@@ -82,5 +82,19 @@ describe Gitlab::Kubernetes::Helm::Parsers::ListV2 do
expect(list_v2.releases).to eq([])
end
end
+
+ context 'invalid Releases' do
+ let(:invalid_file_contents) do
+ '{ "Releases" : ["a", "b"] }'
+ end
+
+ subject(:list_v2) { described_class.new(invalid_file_contents) }
+
+ it 'raises an error' do
+ expect do
+ list_v2.releases
+ end.to raise_error(described_class::ParserError, 'Invalid format for Releases')
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index 99684bb2ab2..4afe4545891 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -86,6 +86,22 @@ describe Gitlab::OmniauthInitializer do
subject.execute([cas3_config])
end
+ it 'configures defaults for google_oauth2' do
+ google_config = {
+ 'name' => 'google_oauth2',
+ "args" => { "access_type" => "offline", "approval_prompt" => '' }
+ }
+
+ expect(devise_config).to receive(:omniauth).with(
+ :google_oauth2,
+ access_type: "offline",
+ approval_prompt: "",
+ client_options: { connection_opts: { request: { timeout: Gitlab::OmniauthInitializer::OAUTH2_TIMEOUT_SECONDS } } }
+ )
+
+ subject.execute([google_config])
+ end
+
it 'converts client_auth_method to a Symbol for openid_connect' do
openid_connect_config = {
'name' => 'openid_connect',
diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb
index 719e98c5fdf..fa4e6288681 100644
--- a/spec/lib/google_api/auth_spec.rb
+++ b/spec/lib/google_api/auth_spec.rb
@@ -40,5 +40,19 @@ describe GoogleApi::Auth do
expect(token).to eq('token')
expect(expires_at).to eq('expires_at')
end
+
+ it 'expects the client to receive default options' do
+ config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
+
+ expect(OAuth2::Client).to receive(:new).with(
+ config.app_id,
+ config.app_secret,
+ hash_including(
+ **config.args.client_options.deep_symbolize_keys
+ )
+ ).and_call_original
+
+ client.get_token('xxx')
+ end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 5b7d69677bf..8273aaeae68 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -590,6 +590,60 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '#find_or_build_application' do
+ let_it_be(:cluster, reload: true) { create(:cluster) }
+
+ it 'rejects classes that are not applications' do
+ expect do
+ cluster.find_or_build_application(Project)
+ end.to raise_error(ArgumentError)
+ end
+
+ context 'when none of applications are created' do
+ it 'returns the new application', :aggregate_failures do
+ described_class::APPLICATIONS.values.each do |application_class|
+ application = cluster.find_or_build_application(application_class)
+
+ expect(application).to be_a(application_class)
+ expect(application).not_to be_persisted
+ end
+ end
+ end
+
+ context 'when application is persisted' do
+ let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
+ let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
+ let!(:cert_manager) { create(:clusters_applications_cert_manager, cluster: cluster) }
+ let!(:crossplane) { create(:clusters_applications_crossplane, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+ let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
+ let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: cluster) }
+ let!(:fluentd) { create(:clusters_applications_fluentd, cluster: cluster) }
+
+ it 'returns the persisted application', :aggregate_failures do
+ {
+ Clusters::Applications::Helm => helm,
+ Clusters::Applications::Ingress => ingress,
+ Clusters::Applications::CertManager => cert_manager,
+ Clusters::Applications::Crossplane => crossplane,
+ Clusters::Applications::Prometheus => prometheus,
+ Clusters::Applications::Runner => runner,
+ Clusters::Applications::Jupyter => jupyter,
+ Clusters::Applications::Knative => knative,
+ Clusters::Applications::ElasticStack => elastic_stack,
+ Clusters::Applications::Fluentd => fluentd
+ }.each do |application_class, expected_object|
+ application = cluster.find_or_build_application(application_class)
+
+ expect(application).to eq(expected_object)
+ expect(application).to be_persisted
+ end
+ end
+ end
+ end
+
describe '#allow_user_defined_namespace?' do
subject { cluster.allow_user_defined_namespace? }
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 73acbd1ea41..097171bbf8e 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -224,6 +224,20 @@ describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ context 'when save fails because the repository could not be created' do
+ before do
+ allow_next_instance_of(Snippets::CreateService) do |instance|
+ allow(instance).to receive(:create_repository).and_raise(Snippets::CreateService::CreateRepositoryError)
+ end
+ end
+
+ it 'returns 400' do
+ post api("/projects/#{project.id}/snippets", admin), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when the snippet is spam' do
def create_snippet(project, snippet_params = {})
project.add_developer(user)
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 8003b8f4b46..074e662adcd 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -267,6 +267,28 @@ describe API::Snippets do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ it 'returns 400 for validation errors' do
+ params[:title] = ''
+
+ post api("/snippets/", user), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ context 'when save fails because the repository could not be created' do
+ before do
+ allow_next_instance_of(Snippets::CreateService) do |instance|
+ allow(instance).to receive(:create_repository).and_raise(Snippets::CreateService::CreateRepositoryError)
+ end
+ end
+
+ it 'returns 400' do
+ post api("/snippets/", user), params: params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when the snippet is spam' do
def create_snippet(snippet_params = {})
post api('/snippets', user), params: params.merge(snippet_params)
@@ -356,6 +378,12 @@ describe API::Snippets do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ it 'returns 400 for validation errors' do
+ update_snippet(params: { title: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
it_behaves_like 'update with repository actions' do
let(:snippet_without_repo) { create(:personal_snippet, author: user, visibility_level: visibility_level) }
end
diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb
index 2d869575d2a..4d49923a184 100644
--- a/spec/services/ci/create_job_artifacts_service_spec.rb
+++ b/spec/services/ci/create_job_artifacts_service_spec.rb
@@ -177,6 +177,53 @@ describe Ci::CreateJobArtifactsService do
end
end
+ context 'when artifact type is cluster_applications' do
+ let(:artifacts_file) do
+ file_to_upload('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz', sha256: artifacts_sha256)
+ end
+
+ let(:params) do
+ {
+ 'artifact_type' => 'cluster_applications',
+ 'artifact_format' => 'gzip'
+ }
+ end
+
+ it 'calls cluster applications parse service' do
+ expect_next_instance_of(Clusters::ParseClusterApplicationsArtifactService) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
+ subject
+ end
+
+ context 'when there is a deployment cluster' do
+ let(:user) { project.owner }
+
+ before do
+ job.update!(user: user)
+ end
+
+ it 'calls cluster applications parse service with job and job user', :aggregate_failures do
+ expect(Clusters::ParseClusterApplicationsArtifactService).to receive(:new).with(job, user).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when ci_synchronous_artifact_parsing feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_synchronous_artifact_parsing: false)
+ end
+
+ it 'does not call parse service' do
+ expect(Clusters::ParseClusterApplicationsArtifactService).not_to receive(:new)
+
+ expect(subject[:status]).to eq(:success)
+ end
+ end
+ end
+
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|
diff --git a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
new file mode 100644
index 00000000000..f14c929554a
--- /dev/null
+++ b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::ParseClusterApplicationsArtifactService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'RELEASE_NAMES' do
+ it 'is included in Cluster application names', :aggregate_failures do
+ described_class::RELEASE_NAMES.each do |release_name|
+ expect(Clusters::Cluster::APPLICATIONS).to include(release_name)
+ end
+ end
+ end
+
+ describe '.new' do
+ let(:job) { build(:ci_build) }
+
+ it 'sets the project and current user', :aggregate_failures do
+ service = described_class.new(job, user)
+
+ expect(service.project).to eq(job.project)
+ expect(service.current_user).to eq(user)
+ end
+ end
+
+ describe '#execute' do
+ let_it_be(:cluster, reload: true) { create(:cluster, projects: [project]) }
+ let_it_be(:deployment, reload: true) { create(:deployment, cluster: cluster) }
+
+ let(:job) { deployment.deployable }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job) }
+
+ context 'when cluster_applications_artifact feature flag is disabled' do
+ before do
+ stub_feature_flags(cluster_applications_artifact: false)
+ end
+
+ it 'does not call Gitlab::Kubernetes::Helm::Parsers::ListV2 and returns success immediately' do
+ expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).not_to receive(:new)
+
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:success)
+ end
+ end
+
+ context 'when cluster_applications_artifact feature flag is enabled for project' do
+ before do
+ stub_feature_flags(cluster_applications_artifact: job.project)
+ end
+
+ it 'calls Gitlab::Kubernetes::Helm::Parsers::ListV2' do
+ expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).to receive(:new).and_call_original
+
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ context 'artifact is not of cluster_applications type' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:job) { artifact.job }
+
+ it 'raise ArgumentError' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to raise_error(ArgumentError, 'Artifact is not cluster_applications file type')
+ end
+ end
+
+ context 'artifact exceeds acceptable size' do
+ it 'returns an error' do
+ stub_const("#{described_class}::MAX_ACCEPTABLE_ARTIFACT_SIZE", 1.byte)
+
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Cluster_applications artifact too big. Maximum allowable size: 1 Byte')
+ end
+ end
+
+ context 'job has no deployment cluster' do
+ let(:job) { build(:ci_build) }
+
+ it 'returns an error' do
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('No deployment cluster found for this job')
+ end
+ end
+
+ context 'job has deployment cluster' do
+ context 'current user does not have access to deployment cluster' do
+ let(:other_user) { create(:user) }
+
+ it 'returns an error' do
+ result = described_class.new(job, other_user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('No deployment cluster found for this job')
+ end
+ end
+
+ context 'release is missing' do
+ let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz' }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'application does not exist' do
+ it 'does not create or destroy an application' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.not_to change(Clusters::Applications::Prometheus, :count)
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'marks the application as uninstalled' do
+ described_class.new(job, user).execute(artifact)
+
+ cluster.application_prometheus.reload
+ expect(cluster.application_prometheus).to be_uninstalled
+ end
+ end
+ end
+
+ context 'release is deployed' do
+ let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz' }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'application does not exist' do
+ it 'creates an application and marks it as installed' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to change(Clusters::Applications::Prometheus, :count)
+
+ expect(cluster.application_prometheus).to be_persisted
+ expect(cluster.application_prometheus).to be_installed
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create(:clusters_applications_prometheus, :errored, cluster: cluster)
+ end
+
+ it 'marks the application as installed' do
+ described_class.new(job, user).execute(artifact)
+
+ expect(cluster.application_prometheus).to be_installed
+ end
+ end
+ end
+
+ context 'release is failed' do
+ let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz' }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'application does not exist' do
+ it 'creates an application and marks it as errored' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to change(Clusters::Applications::Prometheus, :count)
+
+ expect(cluster.application_prometheus).to be_persisted
+ expect(cluster.application_prometheus).to be_errored
+ expect(cluster.application_prometheus.status_reason).to eq('Helm release failed to install')
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'marks the application as errored' do
+ described_class.new(job, user).execute(artifact)
+
+ expect(cluster.application_prometheus).to be_errored
+ expect(cluster.application_prometheus.status_reason).to eq('Helm release failed to install')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index 37f1b33d455..c2fd04d648b 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -194,6 +194,66 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
end
end
+ describe '#make_externally_installed' do
+ subject { create(application_name, :installing) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+
+ context 'application is updated' do
+ subject { create(application_name, :updated) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ context 'application is errored' do
+ subject { create(application_name, :errored) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+ end
+ end
+
+ describe '#make_externally_uninstalled' do
+ subject { create(application_name, :installed) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+
+ context 'application is updated' do
+ subject { create(application_name, :updated) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+ end
+
+ context 'application is errored' do
+ subject { create(application_name, :errored) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+ end
+ end
+
describe '#make_scheduled' do
subject { create(application_name, :installable) }
@@ -278,6 +338,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
:update_errored | false
:uninstalling | false
:uninstall_errored | false
+ :uninstalled | false
:timed_out | false
end
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index dd13a9c166e..f830f957174 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -48,6 +48,28 @@ RSpec.shared_examples 'update with repository actions' do
expect(blob).not_to be_nil
expect(blob.data).to eq content
end
+
+ context 'when save fails due to a repository creation error' do
+ let(:content) { 'File content' }
+ let(:file_name) { 'test.md' }
+
+ before do
+ allow_next_instance_of(Snippets::UpdateService) do |instance|
+ allow(instance).to receive(:create_repository_for).with(snippet).and_raise(Snippets::UpdateService::CreateRepositoryError)
+ end
+
+ update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+ end
+
+ it 'returns 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'does not save the changes to the snippet object' do
+ expect(snippet.content).not_to eq(content)
+ expect(snippet.file_name).not_to eq(file_name)
+ end
+ end
end
end
end
diff --git a/vendor/elastic_stack/wait-for-elasticsearch.sh b/vendor/elastic_stack/wait-for-elasticsearch.sh
index 1423af2e10b..33c5eaae9ef 100755
--- a/vendor/elastic_stack/wait-for-elasticsearch.sh
+++ b/vendor/elastic_stack/wait-for-elasticsearch.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
IFS=$'\n\t'
set -euo pipefail
diff --git a/yarn.lock b/yarn.lock
index 25789a2d4fc..e38111fa438 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -983,10 +983,10 @@
consola "^2.10.1"
node-fetch "^2.6.0"
-"@rails/actioncable@^6.0.2-2":
- version "6.0.2-2"
- resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.2-2.tgz#237907f8111707950381387c273b19ac25958408"
- integrity sha512-0sKStf8hnberH1TKup10PJ92JT2dVqf3gf+OT4lJ7DiYSBEuDcvICHxWsyML2oWTpjUhC4kLvUJ3pXL2JJrJuQ==
+"@rails/actioncable@^6.0.3":
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.3.tgz#722b4b639936129307ddbab3a390f6bcacf3e7bc"
+ integrity sha512-I01hgqxxnOgOtJTGlq0ZsGJYiTEEiSGVEGQn3vimZSqEP1HqzyFNbzGTq14Xdyeow2yGJjygjoFF1pmtE+SQaw==
"@sentry/browser@^5.10.2":
version "5.10.2"