diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index dd23c648bd4..ff8994a9797 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -146,7 +146,7 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
DEBIAN_VERSION: "bullseye"
CHROME_VERSION: "109"
- DOCKER_VERSION: "20.10.14"
+ DOCKER_VERSION: "23.0.1"
RUBY_VERSION: "2.7"
GO_VERSION: "1.18"
RUST_VERSION: "1.65"
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 0ee810340ff..5c7e78b0b67 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -353,7 +353,7 @@
.use-buildx:
extends: .use-docker-in-docker
- image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-slim:docker-${DOCKER_VERSION}-buildx-0.8
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-slim:docker-${DOCKER_VERSION}
variables:
QEMU_IMAGE: tonistiigi/binfmt:qemu-v7.0.0
before_script:
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 369330f8189..7147b31150c 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -95,7 +95,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "afcef7854ac72c5ff958035ef210ba6c68ec800b" # 6.8.0: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/afcef7854ac72c5ff958035ef210ba6c68ec800b
+ GITLAB_HELM_CHART_REF: "febc4ad69acb7bba0eeb4a62daa577d0b7c3ee71" # 6.9.1: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/febc4ad69acb7bba0eeb4a62daa577d0b7c3ee71
environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 6a05bd84830..6c4171da36e 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1359,6 +1359,8 @@
- !reference [".rails:rules:ee-and-foss-default-rules", rules]
- <<: *if-default-refs
changes: *backend-patterns
+ - <<: *if-default-refs
+ changes: *backstage-patterns
.rails:rules:ee-and-foss-unit:predictive:
rules:
@@ -1368,6 +1370,8 @@
- !reference [".rails:rules:unit-integration:predictive-default-rules", rules]
- <<: *if-merge-request
changes: *backend-patterns
+ - <<: *if-merge-request
+ changes: *backstage-patterns
.rails:rules:ee-and-foss-integration:
rules:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b82e5de350f..feaf0e9997a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 15.9.2 (2023-03-02)
+
+### Security (12 changes)
+
+- [Using builds metadata to determine debug_mode](gitlab-org/security/gitlab@e19fcea675071d005eb72c7e100ff0b357f43508) ([merge request](gitlab-org/security/gitlab!3022))
+- [Fix pagination limits for Commits API](gitlab-org/security/gitlab@f71e2650b44e306c8291a8fa5f8557ff4ae4f5d7) ([merge request](gitlab-org/security/gitlab!3071))
+- [Mask Google IAP account details in Prometheus integration](gitlab-org/security/gitlab@8cad41d16614f7eb6a0f1693046ae1981ff413d5) ([merge request](gitlab-org/security/gitlab!3081))
+- [Stop Group Transfer Service if SAML Provider or SCIM token is present](gitlab-org/security/gitlab@e7ebbc1d37372c147392a3854186f4bb7fd15db5) ([merge request](gitlab-org/security/gitlab!3095))
+- [Protect Datadog API key by changing Datadog site](gitlab-org/security/gitlab@abe3343d6cd0397a6b1b491878a9e8dfc5774a2f) ([merge request](gitlab-org/security/gitlab!3093))
+- [Protect integrations' sensitive information exposed via API](gitlab-org/security/gitlab@0036ee57dd9f37858ca09746be20fa254347a7ef) ([merge request](gitlab-org/security/gitlab!3087))
+- [Disallow maintainer to create an owner access token](gitlab-org/security/gitlab@820d02055d2a958462da3be5587d460a905d157f) ([merge request](gitlab-org/security/gitlab!3090))
+- [Paste only text content in work items title](gitlab-org/security/gitlab@5ef125158ceaf0220260423d67cf6a0e1c973e63) ([merge request](gitlab-org/security/gitlab!3074))
+- [Jira DVCS OAuth Open Redirect Vulnerability](gitlab-org/security/gitlab@d6295e117531bc9cde690ba49a456be6883fcd21) ([merge request](gitlab-org/security/gitlab!3077))
+- [Block private personal snippet from unauthorized users](gitlab-org/security/gitlab@1471002b48fba676367397bdffa63a1b50c375bd) ([merge request](gitlab-org/security/gitlab!3079))
+- [Verify Kroki diagram type](gitlab-org/security/gitlab@c76ccc6be3115ded496bbd1bde7da6e4a7dd19ba) ([merge request](gitlab-org/security/gitlab!3056))
+- [Check read_release permission before showing releases in Tags API](gitlab-org/security/gitlab@e176a4eb4d266cf774a06ff021c3789a2cb830d9) ([merge request](gitlab-org/security/gitlab!3060))
+
## 15.9.1 (2023-02-23)
### Fixed (2 changes)
@@ -732,6 +749,23 @@ entry.
- [Remove Gitlab::Redis::DuplicateJobs](gitlab-org/gitlab@73d863b0a49175cce7649c0936b2e16157f61665) ([merge request](gitlab-org/gitlab!109122))
- [Clean-up feature flag `hash_based_cache_for_protected_branches`](gitlab-org/gitlab@96e8a07564bac07a100556e00ce4af3f21dca293) ([merge request](gitlab-org/gitlab!108724))
+## 15.8.4 (2023-03-02)
+
+### Security (12 changes)
+
+- [Using builds metadata to determine debug_mode](gitlab-org/security/gitlab@169fdb3222a9701b5818ef7c00f8f292dc60495d) ([merge request](gitlab-org/security/gitlab!3035))
+- [Fix pagination limits for Commits API](gitlab-org/security/gitlab@3d58c0fef6429d1030d1dfce1ca523ef33a0054b) ([merge request](gitlab-org/security/gitlab!3072))
+- [Mask Google IAP account details in Prometheus integration](gitlab-org/security/gitlab@96426e4c799e9bf5e90e5e57b2e54235831819a3) ([merge request](gitlab-org/security/gitlab!3082))
+- [Stop Group Transfer Service if SAML Provider or SCIM token is present](gitlab-org/security/gitlab@9496a2ed22f73bf83e56b1ff502fefcfe777ad07) ([merge request](gitlab-org/security/gitlab!3097))
+- [Protect Datadog API key by changing Datadog site](gitlab-org/security/gitlab@c6804e50cb60fc4747ea573306eec17eb0dd25f9) ([merge request](gitlab-org/security/gitlab!3094))
+- [Protect integrations' sensitive information exposed via API](gitlab-org/security/gitlab@a408475163272b926e65b1cf56c9efde09eac8dd) ([merge request](gitlab-org/security/gitlab!3088))
+- [Disallow maintainer to create an owner access token](gitlab-org/security/gitlab@d184909f6ab9123a6131c5c37452ace5c4bc8d3d) ([merge request](gitlab-org/security/gitlab!3091))
+- [Paste only text content in work items title](gitlab-org/security/gitlab@d8c48ade46fd75ab62731fced05cdfa2451bcdfa) ([merge request](gitlab-org/security/gitlab!3075))
+- [Jira DVCS OAuth Open Redirect Vulnerability](gitlab-org/security/gitlab@91ee37eeaaae8cc6d923f6b4b28ce0d7914342dd) ([merge request](gitlab-org/security/gitlab!3063))
+- [Block private personal snippet from unauthorized users](gitlab-org/security/gitlab@d687866d69cbdf25a3ca7185974c02402345015d) ([merge request](gitlab-org/security/gitlab!3030))
+- [Verify Kroki diagram type](gitlab-org/security/gitlab@4ec26a4479e73233d0f77bc5a5e764d506c29faf) ([merge request](gitlab-org/security/gitlab!3055))
+- [Check read_release permission before showing releases in Tags API](gitlab-org/security/gitlab@32bf21efc32fcb6a3803993959b50d8a9cd07d25) ([merge request](gitlab-org/security/gitlab!3057))
+
## 15.8.3 (2023-02-15)
### Fixed (3 changes)
@@ -1226,6 +1260,23 @@ No changes.
- [Do not use _test when not necessary](gitlab-org/gitlab@1bde73aba2bd1d7f9e833c7325cffa0c90d1c106) ([merge request](gitlab-org/gitlab!107373))
- [Add config/redis.yml unified config file](gitlab-org/gitlab@ace8301236eecc07a511975b57f80e21ec7be3c2) ([merge request](gitlab-org/gitlab!106854))
+## 15.7.8 (2023-03-02)
+
+### Security (12 changes)
+
+- [Using builds metadata to determine debug_mode](gitlab-org/security/gitlab@12be0c159940a35899851f2867fde1237dae254b) ([merge request](gitlab-org/security/gitlab!3036))
+- [Fix pagination limits for Commits API](gitlab-org/security/gitlab@d507c5d906aff98a8bff943181299cbec5cc43db) ([merge request](gitlab-org/security/gitlab!3073))
+- [Mask Google IAP account details in Prometheus integration](gitlab-org/security/gitlab@54420f92a366e2a7648c10baaaf67492d6676746) ([merge request](gitlab-org/security/gitlab!3083))
+- [Stop Group Transfer Service if SAML Provider or SCIM token is present](gitlab-org/security/gitlab@52400160cd607fb30411dec04b516a1314e44996) ([merge request](gitlab-org/security/gitlab!3098))
+- [Protect Datadog API key by changing Datadog site](gitlab-org/security/gitlab@9aa3ba9f719a786238ae59914d5456666363940e) ([merge request](gitlab-org/security/gitlab!3096))
+- [Protect integrations' sensitive information exposed via API](gitlab-org/security/gitlab@60c22681f52c2aadcb55e1b9e92d358076e3c92c) ([merge request](gitlab-org/security/gitlab!3089))
+- [Disallow maintainer to create an owner access token](gitlab-org/security/gitlab@2adeb7fafb119a43c0bfe162fbc66d2740cb4168) ([merge request](gitlab-org/security/gitlab!3092))
+- [Paste only text content in work items title](gitlab-org/security/gitlab@5fa8a9bf683427af6f25e043b3f0a332719bc970) ([merge request](gitlab-org/security/gitlab!3076))
+- [Jira DVCS OAuth Open Redirect Vulnerability](gitlab-org/security/gitlab@3598b2558de92b0a775f09beb739c6e2f90ff7ab) ([merge request](gitlab-org/security/gitlab!3064))
+- [Block private personal snippet from unauthorized users](gitlab-org/security/gitlab@a106541570423480c9c510f512a2dc61acc5c01f) ([merge request](gitlab-org/security/gitlab!2994))
+- [Verify Kroki diagram type](gitlab-org/security/gitlab@eafe89b8be423e4828fe92769353b7f17ffe895e) ([merge request](gitlab-org/security/gitlab!3054))
+- [Check read_release permission before showing releases in Tags API](gitlab-org/security/gitlab@d56500c47754c7d5eb11f3c84bedbe60366eff0e) ([merge request](gitlab-org/security/gitlab!3058))
+
## 15.7.7 (2023-02-10)
No changes.
diff --git a/Gemfile b/Gemfile
index 9f43ca6daeb..290f762275d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -371,7 +371,7 @@ gem 'prometheus-client-mmap', '~> 0.17', require: 'prometheus/client'
gem 'warning', '~> 1.3.0'
group :development do
- gem 'lefthook', '~> 1.3.2', require: false
+ gem 'lefthook', '~> 1.3.3', require: false
gem 'rubocop'
gem 'solargraph', '~> 0.47.2', require: false
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 89e4073c128..e02aa509807 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -313,7 +313,7 @@
{"name":"kramdown","version":"2.3.2","platform":"ruby","checksum":"cb4530c2e9d16481591df2c9336723683c354e5416a5dd3e447fa48215a6a71c"},
{"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"},
{"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"},
-{"name":"lefthook","version":"1.3.2","platform":"ruby","checksum":"38607be9d670af5bfbbcb2159459f4403bc8e1b10885a923b9e512b3b72b3dec"},
+{"name":"lefthook","version":"1.3.3","platform":"ruby","checksum":"8269a799d0abad6aaf188edb66a661c729abe6b74f3d8d660529d51f9ed2dc5d"},
{"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"},
{"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"},
{"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"},
diff --git a/Gemfile.lock b/Gemfile.lock
index fe25496d125..e1953b90a38 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -836,7 +836,7 @@ GEM
kramdown (~> 2.0)
launchy (2.5.0)
addressable (~> 2.7)
- lefthook (1.3.2)
+ lefthook (1.3.3)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (2.0.0)
@@ -1727,7 +1727,7 @@ DEPENDENCIES
knapsack (~> 1.21.1)
kramdown (~> 2.3.1)
kubeclient (~> 4.9.3)!
- lefthook (~> 1.3.2)
+ lefthook (~> 1.3.3)
letter_opener_web (~> 2.0.0)
license_finder (~> 7.0)
licensee (~> 9.15)
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 7083c5cd0b7..cd372374d83 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,6 +1,5 @@
+
+
+
+ {{ $options.i18n.calendarHint }}
+ ))
+ img_tag = Nokogiri::HTML::DocumentFragment.parse(content_tag(:img, nil, src: image_src))
img_tag = img_tag.children.first
next if img_tag.nil?
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index 95f896a74e9..8a894901ca1 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -44,15 +44,11 @@ module Gitlab
# Overridden in Gitlab::WikiFileFinder
def search_paths(query)
- if Feature.enabled?(:code_basic_search_files_by_regexp, project)
- return [] if query.blank? || ref.blank?
+ return [] if query.blank? || ref.blank?
- escaped_query = RE2::Regexp.escape(query)
- query_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)#{escaped_query}")
- repository.search_files_by_regexp(query_regexp, ref)
- else
- repository.search_files_by_name(query, ref)
- end
+ escaped_query = RE2::Regexp.escape(query)
+ query_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)#{escaped_query}")
+ repository.search_files_by_regexp(query_regexp, ref)
end
end
end
diff --git a/lib/gitlab/http_connection_adapter.rb b/lib/gitlab/http_connection_adapter.rb
index aec430f2686..c6f9f2df299 100644
--- a/lib/gitlab/http_connection_adapter.rb
+++ b/lib/gitlab/http_connection_adapter.rb
@@ -47,7 +47,7 @@ module Gitlab
dns_rebind_protection: dns_rebind_protection?,
schemes: %w[http https])
rescue Gitlab::UrlBlocker::BlockedUrlError => e
- raise Gitlab::HTTP::BlockedUrlError, "URL '#{url}' is blocked: #{e.message}"
+ raise Gitlab::HTTP::BlockedUrlError, "URL is blocked: #{e.message}"
end
def allow_local_requests?
@@ -59,6 +59,8 @@ module Gitlab
end
def dns_rebind_protection?
+ return false if Gitlab.http_proxy_env?
+
Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
end
diff --git a/lib/gitlab/octokit/middleware.rb b/lib/gitlab/octokit/middleware.rb
index 0e47672bb3c..a92860f7eb8 100644
--- a/lib/gitlab/octokit/middleware.rb
+++ b/lib/gitlab/octokit/middleware.rb
@@ -11,8 +11,7 @@ module Gitlab
Gitlab::UrlBlocker.validate!(env[:url],
schemes: %w[http https],
allow_localhost: allow_local_requests?,
- allow_local_network: allow_local_requests?,
- dns_rebind_protection: dns_rebind_protection?
+ allow_local_network: allow_local_requests?
)
@app.call(env)
@@ -23,10 +22,6 @@ module Gitlab
def allow_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
-
- def dns_rebind_protection?
- Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
- end
end
end
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index de952b37b39..13504db0413 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -139,7 +139,7 @@ module Gitlab
end
def enforce_address_info_retrievable?(uri, dns_rebind_protection)
- return false if !dns_rebind_protection || Gitlab.http_proxy_env? || domain_allowed?(uri)
+ return false if !dns_rebind_protection || domain_allowed?(uri)
# In the test suite we use a lot of mocked urls that are either invalid or
# don't exist. In order to avoid modifying a ton of tests and factories
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f89b1fbd69a..bb750079eca 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25547,6 +25547,9 @@ msgstr ""
msgid "Link copied"
msgstr ""
+msgid "Link does not exist"
+msgstr ""
+
msgid "Link text"
msgstr ""
@@ -34995,6 +34998,9 @@ msgstr ""
msgid "ProtectedEnvironment|Allowed to deploy"
msgstr ""
+msgid "ProtectedEnvironment|Allowed to deploy and approve"
+msgstr ""
+
msgid "ProtectedEnvironment|Allowed to deploy to %{project} / %{environment}"
msgstr ""
@@ -45451,6 +45457,9 @@ msgstr ""
msgid "TransferGroup|Group is already associated to the parent group."
msgstr ""
+msgid "TransferGroup|SAML Provider or SCIM Token is configured for this group."
+msgstr ""
+
msgid "TransferGroup|The parent group already has a subgroup or a project with the same path."
msgstr ""
diff --git a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb
index 124b6c9cd44..c50eb2f4fdf 100644
--- a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb
@@ -79,19 +79,24 @@ module QA
'is allowed to commit to sub-group project via the API',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363349'
) do
- expect do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.api_client = parent_group_user_api_client
- commit.project = sub_group_project
- commit.branch = "new_branch_#{SecureRandom.hex(8)}"
- commit.start_branch = sub_group_project.default_branch
- commit.commit_message = 'Add new file'
- commit.add_files([{ file_path: 'test.txt', content: 'new file' }])
- end
- rescue StandardError => e
- QA::Runtime::Logger.error("Full failure message: #{e.message}")
- raise
- end.not_to raise_error
+ # Retry is needed due to delays with project authorization updates
+ # Long term solution to accessing the status of a project authorization update
+ # has been proposed in https://gitlab.com/gitlab-org/gitlab/-/issues/393369
+ QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 2) do
+ expect do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.api_client = parent_group_user_api_client
+ commit.project = sub_group_project
+ commit.branch = "new_branch_#{SecureRandom.hex(8)}"
+ commit.start_branch = sub_group_project.default_branch
+ commit.commit_message = 'Add new file'
+ commit.add_files([{ file_path: 'test.txt', content: 'new file' }])
+ end
+ rescue StandardError => e
+ QA::Runtime::Logger.error("Full failure message: #{e.message}")
+ raise
+ end.not_to raise_error
+ end
end
after do
diff --git a/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb b/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
index 496ef7859f9..3d271a22f27 100644
--- a/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb
@@ -2,24 +2,40 @@
require 'spec_helper'
-RSpec.describe Oauth::JiraDvcs::AuthorizationsController do
+RSpec.describe Oauth::JiraDvcs::AuthorizationsController, feature_category: :integrations do
+ let_it_be(:application) { create(:oauth_application, redirect_uri: 'https://example.com/callback') }
+
describe 'GET new' do
it 'redirects to OAuth authorization with correct params' do
- get :new, params: { client_id: 'client-123', scope: 'foo', redirect_uri: 'http://example.com/' }
+ get :new, params: { client_id: application.uid, scope: 'foo', redirect_uri: 'https://example.com/callback' }
- expect(response).to redirect_to(oauth_authorization_url(client_id: 'client-123',
- response_type: 'code',
- scope: 'foo',
- redirect_uri: oauth_jira_dvcs_callback_url))
+ expect(response).to redirect_to(oauth_authorization_url(
+ client_id: application.uid,
+ response_type: 'code',
+ scope: 'foo',
+ redirect_uri: oauth_jira_dvcs_callback_url))
end
it 'replaces the GitHub "repo" scope with "api"' do
- get :new, params: { client_id: 'client-123', scope: 'repo', redirect_uri: 'http://example.com/' }
+ get :new, params: { client_id: application.uid, scope: 'repo', redirect_uri: 'https://example.com/callback' }
- expect(response).to redirect_to(oauth_authorization_url(client_id: 'client-123',
- response_type: 'code',
- scope: 'api',
- redirect_uri: oauth_jira_dvcs_callback_url))
+ expect(response).to redirect_to(oauth_authorization_url(
+ client_id: application.uid,
+ response_type: 'code',
+ scope: 'api',
+ redirect_uri: oauth_jira_dvcs_callback_url))
+ end
+
+ it 'returns 404 with an invalid client' do
+ get :new, params: { client_id: 'client-123', scope: 'foo', redirect_uri: 'https://example.com/callback' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 403 with an incorrect redirect_uri' do
+ get :new, params: { client_id: application.uid, scope: 'foo', redirect_uri: 'http://unsafe-website.com/callback' }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -47,7 +63,7 @@ RSpec.describe Oauth::JiraDvcs::AuthorizationsController do
double(status: :ok, body: { 'access_token' => 'fake-123', 'scope' => 'foo', 'token_type' => 'bar' })
end
- post :access_token, params: { code: 'code-123', client_id: 'client-123', client_secret: 'secret-123' }
+ post :access_token, params: { code: 'code-123', client_id: application.uid, client_secret: 'secret-123' }
expect(response.body).to eq('access_token=fake-123&scope=foo&token_type=bar')
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index f35d8af165e..c91aa562a85 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -247,7 +247,9 @@ RSpec.describe Projects::ArtifactsController, feature_category: :build_artifacts
let(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
before do
- create(:ci_job_variable, key: 'CI_DEBUG_TRACE', value: 'true', job: job)
+ allow_next_found_instance_of(Ci::Build) do |build|
+ allow(build).to receive(:debug_mode?).and_return(true)
+ end
end
context 'when the user does not have update_build permissions' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index cc4bca8d122..2e29d87dadd 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -630,41 +630,13 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
expect(json_response['lines'].count).to be_positive
end
- context 'when CI_DEBUG_TRACE enabled' do
- let!(:variable) { create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true') }
-
- context 'with proper permissions on a project' do
- let(:user) { developer }
-
- before do
- sign_in(user)
- end
-
- it 'returns response ok' do
- get_trace
-
- expect(response).to have_gitlab_http_status(:ok)
+ context 'when debug_mode? is enabled' do
+ before do
+ allow_next_found_instance_of(Ci::Build) do |build|
+ allow(build).to receive(:debug_mode?).and_return(true)
end
end
- context 'without proper permissions for debug logging' do
- let(:user) { guest }
-
- before do
- sign_in(user)
- end
-
- it 'returns response forbidden' do
- get_trace
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- context 'when CI_DEBUG_SERVICES enabled' do
- let!(:variable) { create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: 'true') }
-
context 'with proper permissions on a project' do
let(:user) { developer }
@@ -1242,10 +1214,10 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
context 'when CI_DEBUG_TRACE and/or CI_DEBUG_SERVICES are enabled' do
using RSpec::Parameterized::TableSyntax
where(:ci_debug_trace, :ci_debug_services) do
- 'true' | 'true'
- 'true' | 'false'
- 'false' | 'true'
- 'false' | 'false'
+ true | true
+ true | false
+ false | true
+ false | false
end
with_them do
@@ -1278,7 +1250,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
it 'returns response forbidden if dev mode enabled' do
response = subject
- if ci_debug_trace == 'true' || ci_debug_services == 'true'
+ if ci_debug_trace || ci_debug_services
expect(response).to have_gitlab_http_status(:forbidden)
else
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index b2a29c88b68..97b0e6d5c48 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -47,12 +47,14 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
def push_code_contribution
event = create(:push_event, project: contributed_project, author: user)
- create(:push_event_payload,
- event: event,
- commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce',
- commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- commit_count: 3,
- ref: 'master')
+ create(
+ :push_event_payload,
+ event: event,
+ commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce',
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 3,
+ ref: 'master'
+ )
end
def note_comment_contribution
@@ -70,162 +72,279 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
find('#js-overview .user-calendar-activities', visible: visible).text
end
- before do
- stub_feature_flags(profile_tabs_vue: false)
- sign_in user
- end
-
- describe 'calendar day selection' do
+ shared_context 'when user page is visited' do
before do
visit user.username
- page.find('.js-overview-tab a').click
+ page.click_link('Overview')
wait_for_requests
end
+ end
+
+ context 'with `profile_tabs_vue` feature flag disabled' do
+ before do
+ stub_feature_flags(profile_tabs_vue: false)
+ sign_in user
+ end
+
+ describe 'calendar day selection' do
+ include_context 'when user page is visited'
+
+ it 'displays calendar' do
+ expect(find('#js-overview')).to have_css('.js-contrib-calendar')
+ end
+
+ describe 'select calendar day' do
+ let(:cells) { page.all('#js-overview .user-contrib-cell') }
+
+ before do
+ cells[0].click
+ wait_for_requests
+ end
+
+ it 'displays calendar day activities' do
+ expect(selected_day_activities).not_to be_empty
+ end
+
+ describe 'select another calendar day' do
+ it 'displays different calendar day activities' do
+ first_day_activities = selected_day_activities
+
+ cells[1].click
+ wait_for_requests
+
+ expect(selected_day_activities).not_to eq(first_day_activities)
+ end
+ end
+
+ describe 'deselect calendar day' do
+ before do
+ cells[0].click
+ wait_for_requests
+ cells[0].click
+ end
+
+ it 'hides calendar day activities' do
+ expect(selected_day_activities(visible: false)).to be_empty
+ end
+ end
+ end
+ end
+
+ describe 'calendar daily activities' do
+ shared_examples 'a day with activity' do |contribution_count:|
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity square for 1 contribution', :sidekiq_inline do
+ expect(find('#js-overview')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
+
+ today = Date.today.strftime(date_format)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+ end
+ end
+
+ describe '1 issue and 1 work item creation calendar activity' do
+ before do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ WorkItems::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: { title: 'new task' },
+ spam_params: nil
+ ).execute
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 2
+
+ describe 'issue title is shown on activity page' do
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity log', :sidekiq_inline do
+ expect(all('#js-overview .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
+ match(/#{issue_title}/),
+ match(/new task/)
+ )
+ end
+ end
+ end
+
+ describe '1 comment calendar activity' do
+ before do
+ note_comment_contribution
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 1
+ end
+
+ describe '10 calendar activities' do
+ before do
+ 10.times { push_code_contribution }
+ end
+
+ it_behaves_like 'a day with activity', contribution_count: 10
+ end
+
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution
+
+ travel_to(Date.yesterday) do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ end
+ end
+
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity squares for both days', :sidekiq_inline do
+ expect(find('#js-overview')).to have_selector(get_cell_level_selector(1), count: 2)
+ end
+
+ it 'displays calendar activity square for yesterday', :sidekiq_inline do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today' do
+ today = Date.today.strftime(date_format)
+ expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
+ end
+ end
+
+ describe 'on smaller screens' do
+ shared_examples 'hidden activity calendar' do
+ include_context 'when user page is visited'
+
+ it 'hides the activity calender' do
+ expect(find('#js-overview')).not_to have_css('.js-contrib-calendar')
+ end
+ end
+
+ context 'when screen size is xs' do
+ before do
+ resize_screen_xs
+ end
+
+ it_behaves_like 'hidden activity calendar'
+ end
+ end
+ end
+
+ context 'with `profile_tabs_vue` feature flag enabled' do
+ before do
+ sign_in user
+ end
+
+ include_context 'when user page is visited'
it 'displays calendar' do
- expect(find('#js-overview')).to have_css('.js-contrib-calendar')
+ expect(page).to have_css('[data-testid="contrib-calendar"]')
end
- describe 'select calendar day' do
- let(:cells) { page.all('#js-overview .user-contrib-cell') }
+ describe 'calendar daily activities' do
+ shared_examples 'a day with activity' do |contribution_count:|
+ include_context 'when user page is visited'
- before do
- cells[0].click
- wait_for_requests
- @first_day_activities = selected_day_activities
+ it 'displays calendar activity square for 1 contribution', :sidekiq_inline do
+ expect(page).to have_selector(get_cell_level_selector(contribution_count), count: 1)
+
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
+ end
end
- it 'displays calendar day activities' do
- expect(selected_day_activities).not_to be_empty
- end
-
- describe 'select another calendar day' do
+ describe '1 issue and 1 work item creation calendar activity' do
before do
- cells[1].click
- wait_for_requests
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ WorkItems::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: { title: 'new task' },
+ spam_params: nil
+ ).execute
end
- it 'displays different calendar day activities' do
- expect(selected_day_activities).not_to eq(@first_day_activities)
- end
+ it_behaves_like 'a day with activity', contribution_count: 2
end
- describe 'deselect calendar day' do
+ describe '1 comment calendar activity' do
before do
- cells[0].click
- wait_for_requests
- cells[0].click
+ note_comment_contribution
end
- it 'hides calendar day activities' do
- expect(selected_day_activities(visible: false)).to be_empty
+ it_behaves_like 'a day with activity', contribution_count: 1
+ end
+
+ describe '10 calendar activities' do
+ before do
+ 10.times { push_code_contribution }
end
- end
- end
- end
- shared_context 'visit user page' do
- before do
- visit user.username
- page.find('.js-overview-tab a').click
- wait_for_requests
- end
- end
-
- describe 'calendar daily activities' do
- shared_examples 'a day with activity' do |contribution_count:|
- include_context 'visit user page'
-
- it 'displays calendar activity square for 1 contribution', :sidekiq_might_not_need_inline do
- expect(find('#js-overview')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
-
- today = Date.today.strftime(date_format)
- expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
- end
- end
-
- describe '1 issue and 1 work item creation calendar activity' do
- before do
- Issues::CreateService.new(container: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
- WorkItems::CreateService.new(
- container: contributed_project,
- current_user: user,
- params: { title: 'new task' },
- spam_params: nil
- ).execute
+ it_behaves_like 'a day with activity', contribution_count: 10
end
- it_behaves_like 'a day with activity', contribution_count: 2
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution
- describe 'issue title is shown on activity page' do
- include_context 'visit user page'
+ travel_to(Date.yesterday) do
+ Issues::CreateService.new(
+ container: contributed_project,
+ current_user: user,
+ params: issue_params,
+ spam_params: nil
+ ).execute
+ end
+ end
- it 'displays calendar activity log', :sidekiq_inline do
- expect(all('#js-overview .overview-content-list .event-target-title').map(&:text)).to contain_exactly(
- match(/#{issue_title}/),
- match(/new task/)
- )
+ include_context 'when user page is visited'
+
+ it 'displays calendar activity squares for both days', :sidekiq_inline do
+ expect(page).to have_selector(get_cell_level_selector(1), count: 2)
+ end
+
+ it 'displays calendar activity square for yesterday', :sidekiq_inline do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today' do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
end
end
end
- describe '1 comment calendar activity' do
- before do
- note_comment_contribution
- end
+ describe 'on smaller screens' do
+ shared_examples 'hidden activity calendar' do
+ include_context 'when user page is visited'
- it_behaves_like 'a day with activity', contribution_count: 1
- end
-
- describe '10 calendar activities' do
- before do
- 10.times { push_code_contribution }
- end
-
- it_behaves_like 'a day with activity', contribution_count: 10
- end
-
- describe 'calendar activity on two days' do
- before do
- push_code_contribution
-
- travel_to(Date.yesterday) do
- Issues::CreateService.new(container: contributed_project, current_user: user, params: issue_params, spam_params: nil).execute
+ it 'hides the activity calender' do
+ expect(page).not_to have_css('[data-testid="contrib-calendar"]')
end
end
- include_context 'visit user page'
- it 'displays calendar activity squares for both days', :sidekiq_might_not_need_inline do
- expect(find('#js-overview')).to have_selector(get_cell_level_selector(1), count: 2)
+ context 'when screen size is xs' do
+ before do
+ resize_screen_xs
+ end
+
+ it_behaves_like 'hidden activity calendar'
end
-
- it 'displays calendar activity square for yesterday', :sidekiq_might_not_need_inline do
- yesterday = Date.yesterday.strftime(date_format)
- expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
- end
-
- it 'displays calendar activity square for today' do
- today = Date.today.strftime(date_format)
- expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, today), count: 1)
- end
- end
- end
-
- describe 'on smaller screens' do
- shared_examples 'hidden activity calendar' do
- include_context 'visit user page'
-
- it 'hides the activity calender' do
- expect(find('#js-overview')).not_to have_css('.js-contrib-calendar')
- end
- end
-
- context 'size xs' do
- before do
- resize_screen_xs
- end
-
- it_behaves_like 'hidden activity calendar'
end
end
end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index c3c0043a6ef..dce86c9f0a4 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -149,109 +149,44 @@ RSpec.describe 'Project Jobs Permissions', feature_category: :projects do
end
end
- context 'with CI_DEBUG_TRACE' do
- let_it_be(:ci_instance_variable) { create(:ci_instance_variable, key: 'CI_DEBUG_TRACE') }
+ describe 'debug_mode' do
+ let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
- describe 'trace endpoint' do
- let_it_be(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
-
- where(:public_builds, :user_project_role, :ci_debug_trace, :expected_status_code) do
- true | 'developer' | true | 200
- true | 'guest' | true | 403
- true | 'developer' | false | 200
- true | 'guest' | false | 200
- false | 'developer' | true | 200
- false | 'guest' | true | 403
- false | 'developer' | false | 200
- false | 'guest' | false | 403
- end
-
- with_them do
- before do
- ci_instance_variable.update!(value: ci_debug_trace)
- project.update!(public_builds: public_builds)
- project.add_role(user, user_project_role)
- end
-
- it 'renders trace to authorized users' do
- visit trace_project_job_path(project, job)
-
- expect(status_code).to eq(expected_status_code)
- end
- end
+ where(:public_builds, :user_project_role, :debug_mode, :expected_status_code, :expected_msg) do
+ true | 'developer' | true | 200 | ''
+ true | 'guest' | true | 403 | 'You must have developer or higher permissions'
+ true | nil | true | 404 | 'Page Not Found Make sure the address is correct'
+ true | 'developer' | false | 200 | ''
+ true | 'guest' | false | 200 | ''
+ true | nil | false | 404 | 'Page Not Found Make sure the address is correct'
+ false | 'developer' | true | 200 | ''
+ false | 'guest' | true | 403 | 'You must have developer or higher permissions'
+ false | nil | true | 404 | 'Page Not Found Make sure the address is correct'
+ false | 'developer' | false | 200 | ''
+ false | 'guest' | false | 403 | 'The current user is not authorized to access the job log'
+ false | nil | false | 404 | 'Page Not Found Make sure the address is correct'
end
- describe 'raw page' do
- let_it_be(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
-
- where(:public_builds, :user_project_role, :ci_debug_trace, :expected_status_code, :expected_msg) do
- true | 'developer' | true | 200 | nil
- true | 'guest' | true | 403 | 'You must have developer or higher permissions'
- true | 'developer' | false | 200 | nil
- true | 'guest' | false | 200 | nil
- false | 'developer' | true | 200 | nil
- false | 'guest' | true | 403 | 'You must have developer or higher permissions'
- false | 'developer' | false | 200 | nil
- false | 'guest' | false | 403 | 'The current user is not authorized to access the job log'
- end
-
- with_them do
- before do
- ci_instance_variable.update!(value: ci_debug_trace)
- project.update!(public_builds: public_builds)
- project.add_role(user, user_project_role)
- end
-
- it 'renders raw trace to authorized users' do
- visit raw_project_job_path(project, job)
-
- expect(status_code).to eq(expected_status_code)
- expect(page).to have_content(expected_msg)
+ with_them do
+ before do
+ project.update!(public_builds: public_builds)
+ user_project_role && project.add_role(user, user_project_role)
+ allow_next_found_instance_of(Ci::Build) do |build|
+ allow(build).to receive(:debug_mode?).and_return(debug_mode)
end
end
- end
- end
- context 'with CI_DEBUG_SERVICES' do
- let_it_be(:ci_instance_variable) { create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES') }
+ it 'renders trace to authorized users' do
+ visit trace_project_job_path(project, job)
- describe 'trace endpoint and raw page' do
- let_it_be(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
-
- where(:public_builds, :user_project_role, :ci_debug_services, :expected_status_code, :expected_msg) do
- true | 'developer' | true | 200 | nil
- true | 'guest' | true | 403 | 'You must have developer or higher permissions'
- true | nil | true | 404 | 'Page Not Found Make sure the address is correct'
- true | 'developer' | false | 200 | nil
- true | 'guest' | false | 200 | nil
- true | nil | false | 404 | 'Page Not Found Make sure the address is correct'
- false | 'developer' | true | 200 | nil
- false | 'guest' | true | 403 | 'You must have developer or higher permissions'
- false | nil | true | 404 | 'Page Not Found Make sure the address is correct'
- false | 'developer' | false | 200 | nil
- false | 'guest' | false | 403 | 'The current user is not authorized to access the job log'
- false | nil | false | 404 | 'Page Not Found Make sure the address is correct'
+ expect(status_code).to eq(expected_status_code)
end
- with_them do
- before do
- ci_instance_variable.update!(value: ci_debug_services)
- project.update!(public_builds: public_builds)
- user_project_role && project.add_role(user, user_project_role)
- end
+ it 'renders raw trace to authorized users' do
+ visit raw_project_job_path(project, job)
- it 'renders trace to authorized users' do
- visit trace_project_job_path(project, job)
-
- expect(status_code).to eq(expected_status_code)
- end
-
- it 'renders raw trace to authorized users' do
- visit raw_project_job_path(project, job)
-
- expect(status_code).to eq(expected_status_code)
- expect(page).to have_content(expected_msg)
- end
+ expect(status_code).to eq(expected_status_code)
+ expect(page).to have_content(expected_msg)
end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 61be90b267a..792a14e3064 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -148,15 +148,6 @@ RSpec.describe NotesFinder do
expect(notes.count).to eq(1)
end
- it 'finds notes on personal snippets' do
- note = create(:note_on_personal_snippet)
- params = { project: project, target_type: 'personal_snippet', target_id: note.noteable_id }
-
- notes = described_class.new(user, params).execute
-
- expect(notes.count).to eq(1)
- end
-
it 'raises an exception for an invalid target_type' do
params[:target_type] = 'invalid'
expect { described_class.new(user, params).execute }.to raise_error("invalid target_type '#{params[:target_type]}'")
@@ -190,6 +181,44 @@ RSpec.describe NotesFinder do
expect { described_class.new(user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ context 'when targeting personal_snippet' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:author) { create(:user) }
+ let(:user) { create(:user, email: 'foo@baz.com') }
+ let(:admin) { create(:admin) }
+
+ where(:snippet_visibility, :current_user, :access) do
+ Snippet::PRIVATE | ref(:author) | true
+ Snippet::PRIVATE | ref(:admin) | true
+ Snippet::PRIVATE | ref(:user) | false
+ Snippet::PUBLIC | ref(:author) | true
+ Snippet::PUBLIC | ref(:user) | true
+ end
+
+ with_them do
+ let(:personal_snippet) { create(:personal_snippet, author: author, visibility_level: snippet_visibility) }
+ let(:note) { create(:note, noteable: personal_snippet) }
+ let(:params) { { project: project, target_type: 'personal_snippet', target_id: note.noteable.id } }
+
+ subject(:notes) do
+ described_class.new(current_user, params).execute
+ end
+
+ before do
+ allow(admin).to receive(:can_read_all_resources?).and_return(true)
+ end
+
+ it 'returns the proper access' do
+ if access
+ expect(notes.count).to eq(1)
+ else
+ expect { notes }.to raise_error(::ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+ end
end
context 'for explicit target' do
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 9c9a04a4df5..48880ec2c1f 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -231,22 +231,31 @@ RSpec.describe SnippetsFinder do
context 'filter by snippet type' do
context 'when filtering by only_personal snippet', :enable_admin_mode do
- it 'returns only personal snippet' do
+ let!(:admin_private_personal_snippet) { create(:personal_snippet, :private, author: admin) }
+ let(:user_without_snippets) { create :user }
+
+ it 'returns all personal snippets for the admin' do
snippets = described_class.new(admin, only_personal: true).execute
+ expect(snippets).to contain_exactly(admin_private_personal_snippet,
+ private_personal_snippet,
+ internal_personal_snippet,
+ public_personal_snippet)
+ end
+
+ it 'returns only personal snippets visible by user' do
+ snippets = described_class.new(user, only_personal: true).execute
+
expect(snippets).to contain_exactly(private_personal_snippet,
internal_personal_snippet,
public_personal_snippet)
end
- end
- context 'when filtering by only_project snippet', :enable_admin_mode do
- it 'returns only project snippet' do
- snippets = described_class.new(admin, only_project: true).execute
+ it 'returns only internal or public personal snippets for user without snippets' do
+ snippets = described_class.new(user_without_snippets, only_personal: true).execute
- expect(snippets).to contain_exactly(private_project_snippet,
- internal_project_snippet,
- public_project_snippet)
+ expect(snippets).to contain_exactly(internal_personal_snippet,
+ public_personal_snippet)
end
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json
index bb0190955f0..71e6c0ec035 100644
--- a/spec/fixtures/api/schemas/public_api/v4/tag.json
+++ b/spec/fixtures/api/schemas/public_api/v4/tag.json
@@ -3,8 +3,7 @@
"required" : [
"name",
"message",
- "commit",
- "release"
+ "commit"
],
"properties" : {
"name": { "type": "string" },
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 1c2e7033488..58e074b5b2d 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,6 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -9,7 +8,6 @@ import DesignNote from '~/design_management/components/design_notes/design_note.
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
-import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import destroyNoteMutation from '~/design_management/graphql/mutations/destroy_note.mutation.graphql';
@@ -40,18 +38,7 @@ describe('Design discussions component', () => {
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
- const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
- const mutationVariables = {
- mutation: createNoteMutation,
- variables: {
- input: {
- noteableId: 'noteable-id',
- body: 'test',
- discussionId: '0',
- },
- },
- };
const registerPath = '/users/sign_up?redirect_to_referer=yes';
const signInPath = '/users/sign_in?redirect_to_referer=yes';
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
@@ -222,7 +209,7 @@ describe('Design discussions component', () => {
it('emit todo:toggle when discussion is resolved', async () => {
createComponent({
props: { discussionWithOpenForm: defaultMockDiscussion.id },
- data: { discussionComment: 'test', isFormRendered: true },
+ data: { isFormRendered: true },
});
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -275,16 +262,14 @@ describe('Design discussions component', () => {
expect(findReplyForm().exists()).toBe(true);
});
- it('calls mutation on submitting form and closes the form', async () => {
+ it('closes the form when note submit mutation is completed', async () => {
createComponent({
props: { discussionWithOpenForm: defaultMockDiscussion.id },
- data: { discussionComment: 'test', isFormRendered: true },
+ data: { isFormRendered: true },
});
- findReplyForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ findReplyForm().vm.$emit('note-submit-complete', { data: { createNote: {} } });
- await mutate();
await nextTick();
expect(findReplyForm().exists()).toBe(false);
@@ -293,14 +278,12 @@ describe('Design discussions component', () => {
it('clears the discussion comment on closing comment form', async () => {
createComponent({
props: { discussionWithOpenForm: defaultMockDiscussion.id },
- data: { discussionComment: 'test', isFormRendered: true },
+ data: { isFormRendered: true },
});
await nextTick();
findReplyForm().vm.$emit('cancel-form');
- expect(wrapper.vm.discussionComment).toBe('');
-
await nextTick();
expect(findReplyForm().exists()).toBe(false);
});
@@ -345,7 +328,7 @@ describe('Design discussions component', () => {
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
createComponent({
props: { discussionWithOpenForm: defaultMockDiscussion.id },
- data: { discussionComment: 'test', isFormRendered: true },
+ data: { isFormRendered: true },
});
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -380,7 +363,7 @@ describe('Design discussions component', () => {
},
discussionWithOpenForm: defaultMockDiscussion.id,
},
- data: { discussionComment: 'test', isFormRendered: true },
+ data: { isFormRendered: true },
});
});
@@ -392,10 +375,6 @@ describe('Design discussions component', () => {
expect(findReplyPlaceholder().exists()).toBe(false);
});
- it('does not render apollo-mutation component', () => {
- expect(findApolloMutation().exists()).toBe(false);
- });
-
it('renders design-note-signed-out component', () => {
expect(findDesignNoteSignedOut().exists()).toBe(true);
expect(findDesignNoteSignedOut().props()).toMatchObject({
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index ba4ee3cbe03..eda231fc468 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -168,15 +168,21 @@ describe('Design note component', () => {
expect(findNoteContent().exists()).toBe(true);
});
- it('calls a mutation on submit-form event and hides a form', async () => {
- findReplyForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalled();
+ it('hides a form after update mutation is completed', async () => {
+ findReplyForm().vm.$emit('note-submit-complete', { data: { updateNote: { errors: [] } } });
- await mutate();
await nextTick();
expect(findReplyForm().exists()).toBe(false);
expect(findNoteContent().exists()).toBe(true);
});
+
+ it('emits error on failure', async () => {
+ const mockError = 'error';
+ findReplyForm().vm.$emit('note-submit-failure', mockError);
+
+ await nextTick();
+ expect(wrapper.emitted('error')).toEqual([[mockError]]);
+ });
});
});
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index f4d4f9cf896..a1da2ec1300 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -1,8 +1,14 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Autosave from '~/autosave';
+import waitForPromises from 'helpers/wait_for_promises';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import {
+ mockNoteSubmitSuccessMutationResponse,
+ mockNoteSubmitFailureMutationResponse,
+} from '../../mock_data/apollo_mock';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/autosave');
@@ -15,15 +21,45 @@ describe('Design reply form component', () => {
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
- function createComponent(props = {}, mountOptions = {}) {
+ const mockNoteableId = 'gid://gitlab/DesignManagement::Design/6';
+ const mockComment = 'New comment';
+ const mockDiscussionId = 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8';
+ const createNoteMutationData = {
+ mutation: createNoteMutation,
+ update: expect.anything(),
+ variables: {
+ input: {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ body: mockComment,
+ },
+ },
+ };
+
+ const ctrlKey = {
+ ctrlKey: true,
+ };
+ const metaKey = {
+ metaKey: true,
+ };
+ const mutationHandler = jest.fn().mockResolvedValue();
+
+ function createComponent({ props = {}, mountOptions = {}, mutation = mutationHandler } = {}) {
wrapper = mount(DesignReplyForm, {
propsData: {
+ designNoteMutation: createNoteMutation,
+ noteableId: mockNoteableId,
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
value: '',
- isSaving: false,
- noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
...mountOptions,
+ mocks: {
+ $apollo: {
+ mutate: mutation,
+ },
+ },
});
}
@@ -40,7 +76,7 @@ describe('Design reply form component', () => {
it('textarea has focus after component mount', () => {
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
- createComponent({}, { attachTo: document.body });
+ createComponent({ mountOptions: { attachTo: document.body } });
expect(findTextarea().element).toEqual(document.activeElement);
});
@@ -64,7 +100,7 @@ describe('Design reply form component', () => {
});
it('renders button text as "Save comment" when creating a comment', () => {
- createComponent({ isNewComment: false });
+ createComponent({ props: { isNewComment: false } });
expect(findSubmitButton().html()).toMatchSnapshot();
});
@@ -76,7 +112,7 @@ describe('Design reply form component', () => {
`(
'initializes autosave support on discussion with proper key',
async ({ discussionId, shortDiscussionId }) => {
- createComponent({ discussionId });
+ createComponent({ props: { discussionId } });
await nextTick();
expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
@@ -88,32 +124,24 @@ describe('Design reply form component', () => {
);
describe('when form has no text', () => {
- beforeEach(() => {
- createComponent({
- value: '',
- });
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
});
it('submit button is disabled', () => {
expect(findSubmitButton().attributes().disabled).toBe('disabled');
});
- it('does not emit submitForm event on textarea ctrl+enter keydown', async () => {
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
- });
+ it.each`
+ key | keyData
+ ${'ctrl'} | ${ctrlKey}
+ ${'meta'} | ${metaKey}
+ `('does not perform mutation on textarea $key+enter keydown', async ({ keyData }) => {
+ findTextarea().trigger('keydown.enter', keyData);
await nextTick();
- expect(wrapper.emitted('submit-form')).toBeUndefined();
- });
-
- it('does not emit submitForm event on textarea meta+enter keydown', async () => {
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
- });
-
- await nextTick();
- expect(wrapper.emitted('submit-form')).toBeUndefined();
+ expect(mutationHandler).not.toHaveBeenCalled();
});
it('emits cancelForm event on pressing escape button on textarea', () => {
@@ -129,118 +157,148 @@ describe('Design reply form component', () => {
});
});
- describe('when form has text', () => {
- beforeEach(() => {
- createComponent({
- value: 'test',
- });
- });
-
+ describe('when the form has text', () => {
it('submit button is enabled', () => {
+ createComponent({ props: { value: mockComment } });
expect(findSubmitButton().attributes().disabled).toBeUndefined();
});
- it('emits submitForm event on Comment button click', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ it('calls a mutation on submit button click event', async () => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
+ const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
+ mutation: successfulMutation,
+ });
findSubmitButton().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
+ expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData);
+
+ await waitForPromises();
+ expect(wrapper.emitted('note-submit-complete')).toEqual([
+ [mockNoteSubmitSuccessMutationResponse],
+ ]);
});
- it('emits submitForm event on textarea ctrl+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
-
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
+ it.each`
+ key | keyData
+ ${'ctrl'} | ${ctrlKey}
+ ${'meta'} | ${metaKey}
+ `('does perform mutation on textarea $key+enter keydown', async ({ keyData }) => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
+ const successfulMutation = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
+ mutation: successfulMutation,
});
+ findTextarea().trigger('keydown.enter', keyData);
+
await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
+ expect(successfulMutation).toHaveBeenCalledWith(createNoteMutationData);
+
+ await waitForPromises();
+ expect(wrapper.emitted('note-submit-complete')).toEqual([
+ [mockNoteSubmitSuccessMutationResponse],
+ ]);
});
- it('emits submitForm event on textarea meta+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
-
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
+ it('emits error when mutation fails', async () => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
+ const failedMutation = jest.fn().mockRejectedValue(mockNoteSubmitFailureMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
+ mutation: failedMutation,
});
- await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
- });
+ findSubmitButton().vm.$emit('click');
- it('emits input event on changing textarea content', async () => {
- findTextarea().setValue('test2');
-
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['test2']]);
+ await waitForPromises();
+ expect(wrapper.emitted('note-submit-failure')).toEqual([
+ [mockNoteSubmitFailureMutationResponse],
+ ]);
});
it('emits cancelForm event on Escape key if text was not changed', () => {
+ createComponent();
+
findTextarea().trigger('keyup.esc');
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
it('opens confirmation modal on Escape key when text has changed', async () => {
- wrapper.setProps({ value: 'test2' });
+ createComponent();
+
+ findTextarea().setValue(mockComment);
await nextTick();
findTextarea().trigger('keyup.esc');
- expect(confirmAction).toHaveBeenCalled();
- });
- it('emits cancelForm event on Cancel button click if text was not changed', () => {
- findCancelButton().trigger('click');
-
- expect(wrapper.emitted('cancel-form')).toHaveLength(1);
- });
-
- it('opens confirmation modal on Cancel button click when text has changed', async () => {
- wrapper.setProps({ value: 'test2' });
-
- await nextTick();
- findCancelButton().trigger('click');
expect(confirmAction).toHaveBeenCalled();
});
it('emits cancelForm event when confirmed', async () => {
confirmAction.mockResolvedValueOnce(true);
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
- wrapper.setProps({ value: 'test3' });
+ createComponent({ props: { value: mockComment } });
+ findTextarea().setValue('Comment changed');
+
await nextTick();
-
findTextarea().trigger('keyup.esc');
- await nextTick();
expect(confirmAction).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
});
- it("doesn't emit cancelForm event when not confirmed", async () => {
+ it('does not emit cancelForm event when not confirmed', async () => {
confirmAction.mockResolvedValueOnce(false);
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
- wrapper.setProps({ value: 'test3' });
+ createComponent({ props: { value: mockComment } });
+ findTextarea().setValue('Comment changed');
await nextTick();
findTextarea().trigger('keyup.esc');
await nextTick();
expect(confirmAction).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('cancel-form')).toBeUndefined();
- expect(autosaveResetSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when component is destroyed', () => {
+ it('calls autosave.reset', async () => {
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ createComponent();
+ await wrapper.destroy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 2a43b5debee..a90bbe19a4a 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -211,3 +211,109 @@ export const getDesignQueryResponse = {
},
},
};
+
+export const mockNoteSubmitSuccessMutationResponse = [
+ {
+ data: {
+ createNote: {
+ note: {
+ id: 'gid://gitlab/DiffNote/468',
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ body: 'New comment',
+ bodyHtml: "
asdd
", + createdAt: '2023-02-24T06:49:20Z', + resolved: false, + position: { + diffRefs: { + baseSha: 'f63ae53ed82d8765477c191383e1e6a000c10375', + startSha: 'f63ae53ed82d8765477c191383e1e6a000c10375', + headSha: 'f348c652f1a737151fc79047895e695fbe81464c', + __typename: 'DiffRefs', + }, + x: 441, + y: 128, + height: 152, + width: 695, + __typename: 'DiffPosition', + }, + userPermissions: { + adminNote: true, + repositionNote: true, + __typename: 'NotePermissions', + }, + discussion: { + id: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8', + notes: { + nodes: [ + { + id: 'gid://gitlab/DiffNote/459', + __typename: 'Note', + }, + ], + __typename: 'NoteConnection', + }, + __typename: 'Discussion', + }, + __typename: 'Note', + }, + errors: [], + __typename: 'CreateNotePayload', + }, + }, + }, +]; + +export const mockNoteSubmitFailureMutationResponse = [ + { + errors: [ + { + message: + 'Variable $input of type CreateNoteInput! was provided invalid value for bodyaa (Field is not defined on CreateNoteInput), body (Expected value to not be null)', + locations: [ + { + line: 1, + column: 21, + }, + ], + extensions: { + value: { + noteableId: 'gid://gitlab/DesignManagement::Design/10', + discussionId: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8', + bodyaa: 'df', + }, + problems: [ + { + path: ['bodyaa'], + explanation: 'Field is not defined on CreateNoteInput', + }, + { + path: ['body'], + explanation: 'Expected value to not be null', + }, + ], + }, + }, + ], + }, +]; + +export const mockCreateImageNoteDiffResponse = { + data: { + createImageDiffNote: { + note: { + author: { + username: '', + }, + discussion: {}, + }, + }, + }, +}; diff --git a/spec/frontend/design_management/mock_data/project.js b/spec/frontend/design_management/mock_data/project.js new file mode 100644 index 00000000000..e1c2057d8d1 --- /dev/null +++ b/spec/frontend/design_management/mock_data/project.js @@ -0,0 +1,17 @@ +import design from './design'; + +export default { + project: { + issue: { + designCollection: { + designs: { + nodes: [ + { + ...design, + }, + ], + }, + }, + }, + }, +}; diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js index a11463ab663..72b143d3789 100644 --- a/spec/frontend/design_management/pages/design/index_spec.js +++ b/spec/frontend/design_management/pages/design/index_spec.js @@ -1,15 +1,14 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; -import { ApolloMutation } from 'vue-apollo'; import VueRouter from 'vue-router'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import Api from '~/api'; import DesignPresentation from '~/design_management/components/design_presentation.vue'; import DesignSidebar from '~/design_management/components/design_sidebar.vue'; import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants'; -import createImageDiffNoteMutation from '~/design_management/graphql/mutations/create_image_diff_note.mutation.graphql'; import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql'; +import getDesignQuery from '~/design_management/graphql/queries/get_design.query.graphql'; import DesignIndex from '~/design_management/pages/design/index.vue'; import createRouter from '~/design_management/router'; import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants'; @@ -17,6 +16,7 @@ import * as utils from '~/design_management/utils/design_management_utils'; import { DESIGN_NOT_FOUND_ERROR, DESIGN_VERSION_NOT_EXIST_ERROR, + ADD_IMAGE_DIFF_NOTE_ERROR, } from '~/design_management/utils/error_messages'; import { DESIGN_TRACKING_PAGE_NAME, @@ -24,15 +24,22 @@ import { DESIGN_SERVICE_PING_EVENT_TYPES, } from '~/design_management/utils/tracking'; import { createAlert } from '~/flash'; +import * as cacheUpdate from '~/design_management/utils/cache_update'; import mockAllVersions from '../../mock_data/all_versions'; import design from '../../mock_data/design'; +import mockProject from '../../mock_data/project'; import mockResponseWithDesigns from '../../mock_data/designs'; import mockResponseNoDesigns from '../../mock_data/no_designs'; +import { mockCreateImageNoteDiffResponse } from '../../mock_data/apollo_mock'; jest.mock('~/flash'); jest.mock('~/api.js'); const focusInput = jest.fn(); +const mockCacheObject = { + readQuery: jest.fn().mockReturnValue(mockProject), + writeQuery: jest.fn(), +}; const mutate = jest.fn().mockResolvedValue(); const mockPageLayoutElement = { classList: { @@ -52,32 +59,13 @@ const mockDesignNoDiscussions = { nodes: [], }, }; -const newComment = 'new comment'; + const annotationCoordinates = { x: 10, y: 10, width: 100, height: 100, }; -const createDiscussionMutationVariables = { - mutation: createImageDiffNoteMutation, - update: expect.anything(), - variables: { - input: { - body: newComment, - noteableId: design.id, - position: { - headSha: 'headSha', - baseSha: 'baseSha', - startSha: 'startSha', - paths: { - newPath: 'full-design-path', - }, - ...annotationCoordinates, - }, - }, - }, -}; Vue.use(VueRouter); @@ -85,8 +73,9 @@ describe('Design management design index page', () => { let wrapper; let router; - const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm); + const findDesignReplyForm = () => wrapper.findComponent(DesignReplyForm); const findSidebar = () => wrapper.findComponent(DesignSidebar); + const findAlert = () => wrapper.findComponent(GlAlert); const findDesignPresentation = () => wrapper.findComponent(DesignPresentation); function createComponent( @@ -95,7 +84,7 @@ describe('Design management design index page', () => { data = {}, intialRouteOptions = {}, provide = {}, - stubs = { ApolloMutation, DesignSidebar, DesignReplyForm }, + stubs = { DesignSidebar, DesignReplyForm }, } = {}, ) { const $apollo = { @@ -105,6 +94,11 @@ describe('Design management design index page', () => { }, }, mutate, + getClient() { + return { + cache: mockCacheObject, + }; + }, }; router = createRouter(); @@ -216,7 +210,7 @@ describe('Design management design index page', () => { findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 }); await nextTick(); - expect(findDiscussionForm().exists()).toBe(true); + expect(findDesignReplyForm().exists()).toBe(true); }); it('keeps new discussion form focused', () => { @@ -235,24 +229,53 @@ describe('Design management design index page', () => { expect(focusInput).toHaveBeenCalled(); }); - it('sends a mutation on submitting form and closes form', async () => { + it('sends a update and closes the form when mutation is completed', async () => { createComponent( { loading: false }, { data: { design, annotationCoordinates, - comment: newComment, }, }, ); - findDiscussionForm().vm.$emit('submit-form'); - expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables); + const addImageDiffNoteToStore = jest.spyOn(cacheUpdate, 'updateStoreAfterAddImageDiffNote'); + + const mockDesignVariables = { + fullPath: 'project-path', + iid: '1', + filenames: ['gid::/gitlab/Design/1'], + atVersion: null, + }; + + findDesignReplyForm().vm.$emit('note-submit-complete', mockCreateImageNoteDiffResponse); await nextTick(); - await mutate({ variables: createDiscussionMutationVariables }); - expect(findDiscussionForm().exists()).toBe(false); + expect(addImageDiffNoteToStore).toHaveBeenCalledWith( + mockCacheObject, + mockCreateImageNoteDiffResponse.data.createImageDiffNote, + getDesignQuery, + mockDesignVariables, + ); + expect(findDesignReplyForm().exists()).toBe(false); + }); + + it('sets error message when form submission fails', async () => { + createComponent( + { loading: false }, + { + data: { + design, + annotationCoordinates, + }, + }, + ); + + findDesignReplyForm().vm.$emit('note-submit-failure'); + + await nextTick(); + expect(findAlert().text()).toBe(ADD_IMAGE_DIFF_NOTE_ERROR); }); it('closes the form and clears the comment on canceling form', async () => { @@ -262,17 +285,14 @@ describe('Design management design index page', () => { data: { design, annotationCoordinates, - comment: newComment, }, }, ); - findDiscussionForm().vm.$emit('cancel-form'); - - expect(wrapper.vm.comment).toBe(''); + findDesignReplyForm().vm.$emit('cancel-form'); await nextTick(); - expect(findDiscussionForm().exists()).toBe(false); + expect(findDesignReplyForm().exists()).toBe(false); }); describe('with error', () => { diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js index 7560b733ae6..0699846e4bb 100644 --- a/spec/frontend/issues/show/components/title_spec.js +++ b/spec/frontend/issues/show/components/title_spec.js @@ -1,94 +1,99 @@ -import Vue, { nextTick } from 'vue'; +import { nextTick } from 'vue'; +import { GlButton } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import titleComponent from '~/issues/show/components/title.vue'; +import Title from '~/issues/show/components/title.vue'; import eventHub from '~/issues/show/event_hub'; -import Store from '~/issues/show/stores'; describe('Title component', () => { - let vm; - beforeEach(() => { + let wrapper; + + const getTitleHeader = () => wrapper.findByTestId('issue-title'); + const getEditButton = () => wrapper.findComponent(GlButton); + + const createWrapper = (props) => { setHTMLFixture(`xss))
+
+ expect(doc.to_s).to eq %(xss)
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index 7144d23b079..f8986e8fa10 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -184,7 +184,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi
let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' }
it 'includes details about blocked URL' do
- expect(subject).to eq "Remote file could not be fetched because URL '#{location}' " \
+ expect(subject).to eq "Remote file could not be fetched because URL " \
'is blocked: Requests to localhost are not allowed!'
end
end
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 27750f10e87..8afaec3c381 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -13,124 +13,58 @@ RSpec.describe Gitlab::FileFinder, feature_category: :global_search do
let(:expected_file_by_content) { 'CHANGELOG' }
end
- context 'when code_basic_search_files_by_regexp is enabled' do
- before do
- stub_feature_flags(code_basic_search_files_by_regexp: true)
+ context 'with inclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files filename:wm.svg')
+
+ expect(results.count).to eq(1)
end
- context 'with inclusive filters' do
- it 'filters by filename' do
- results = subject.find('files filename:wm.svg')
+ it 'filters by path' do
+ results = subject.find('white path:images')
- expect(results.count).to eq(1)
- end
-
- it 'filters by path' do
- results = subject.find('white path:images')
-
- expect(results.count).to eq(2)
- end
-
- it 'filters by extension' do
- results = subject.find('files extension:md')
-
- expect(results.count).to eq(4)
- end
+ expect(results.count).to eq(2)
end
- context 'with exclusive filters' do
- it 'filters by filename' do
- results = subject.find('files -filename:wm.svg')
+ it 'filters by extension' do
+ results = subject.find('files extension:md')
- expect(results.count).to eq(26)
- end
-
- it 'filters by path' do
- results = subject.find('white -path:images')
-
- expect(results.count).to eq(5)
- end
-
- it 'filters by extension' do
- results = subject.find('files -extension:md')
-
- expect(results.count).to eq(23)
- end
- end
-
- context 'with white space in the path' do
- it 'filters by path correctly' do
- results = subject.find('directory path:"with space/README.md"')
-
- expect(results.count).to eq(1)
- end
- end
-
- it 'does not cause N+1 query' do
- expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
-
- subject.find(': filename:wm.svg')
+ expect(results.count).to eq(4)
end
end
- context 'when code_basic_search_files_by_regexp is disabled' do
- before do
- stub_feature_flags(code_basic_search_files_by_regexp: false)
+ context 'with exclusive filters' do
+ it 'filters by filename' do
+ results = subject.find('files -filename:wm.svg')
+
+ expect(results.count).to eq(26)
end
- context 'with inclusive filters' do
- it 'filters by filename' do
- results = subject.find('files filename:wm.svg')
+ it 'filters by path' do
+ results = subject.find('white -path:images')
- expect(results.count).to eq(1)
- end
-
- it 'filters by path' do
- results = subject.find('white path:images')
-
- expect(results.count).to eq(1)
- end
-
- it 'filters by extension' do
- results = subject.find('files extension:md')
-
- expect(results.count).to eq(4)
- end
+ expect(results.count).to eq(5)
end
- context 'with exclusive filters' do
- it 'filters by filename' do
- results = subject.find('files -filename:wm.svg')
+ it 'filters by extension' do
+ results = subject.find('files -extension:md')
- expect(results.count).to eq(26)
- end
-
- it 'filters by path' do
- results = subject.find('white -path:images')
-
- expect(results.count).to eq(4)
- end
-
- it 'filters by extension' do
- results = subject.find('files -extension:md')
-
- expect(results.count).to eq(23)
- end
+ expect(results.count).to eq(23)
end
+ end
- context 'with white space in the path' do
- it 'filters by path correctly' do
- results = subject.find('directory path:"with space/README.md"')
+ context 'with white space in the path' do
+ it 'filters by path correctly' do
+ results = subject.find('directory path:"with space/README.md"')
- expect(results.count).to eq(1)
- end
+ expect(results.count).to eq(1)
end
+ end
- it 'does not cause N+1 query' do
- expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
+ it 'does not cause N+1 query' do
+ expect(Gitlab::GitalyClient).to receive(:call).at_most(10).times.and_call_original
- subject.find(': filename:wm.svg')
- end
+ subject.find(': filename:wm.svg')
end
end
end
diff --git a/spec/lib/gitlab/fogbugz_import/importer_spec.rb b/spec/lib/gitlab/fogbugz_import/importer_spec.rb
index 9b58b772d1a..a4246809725 100644
--- a/spec/lib/gitlab/fogbugz_import/importer_spec.rb
+++ b/spec/lib/gitlab/fogbugz_import/importer_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
expect { subject.execute }
.to raise_error(
::Gitlab::HTTP::BlockedUrlError,
- "URL 'https://localhost:3000/api.asp' is blocked: Requests to localhost are not allowed"
+ "URL is blocked: Requests to localhost are not allowed"
)
end
end
@@ -84,7 +84,7 @@ RSpec.describe Gitlab::FogbugzImport::Importer do
expect { subject.execute }
.to raise_error(
::Gitlab::HTTP::BlockedUrlError,
- "URL 'http://192.168.0.1/api.asp' is blocked: Requests to the local network are not allowed"
+ "URL is blocked: Requests to the local network are not allowed"
)
end
end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
index 5137e098e2d..dbf0252da46 100644
--- a/spec/lib/gitlab/http_connection_adapter_spec.rb
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
it 'raises error' do
expect { subject }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://172.16.0.0/12' is blocked: Requests to the local network are not allowed"
+ "URL is blocked: Requests to the local network are not allowed"
)
end
@@ -67,7 +67,7 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
it 'raises error' do
expect { subject }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed"
+ "URL is blocked: Requests to localhost are not allowed"
)
end
@@ -111,13 +111,27 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
end
end
+ context 'when http(s) environment variable is set' do
+ before do
+ stub_env('https_proxy' => 'https://my.proxy')
+ end
+
+ it 'sets up the connection' do
+ expect(connection).to be_a(Gitlab::NetHttpAdapter)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
context 'when URL scheme is not HTTP/HTTPS' do
let(:uri) { URI('ssh://example.org') }
it 'raises error' do
expect { subject }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'ssh://example.org' is blocked: Only allowed schemes are http, https"
+ "URL is blocked: Only allowed schemes are http, https"
)
end
end
diff --git a/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb b/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb
index b1bc6b7eeaf..3d9d6e1b96b 100644
--- a/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb
+++ b/spec/lib/gitlab/import_export/remote_stream_upload_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Gitlab::ImportExport::RemoteStreamUpload do
it 'raises error' do
expect { subject.execute }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://127.0.0.1/file.txt' is blocked: Requests to localhost are not allowed"
+ "URL is blocked: Requests to localhost are not allowed"
)
end
@@ -114,7 +114,7 @@ RSpec.describe Gitlab::ImportExport::RemoteStreamUpload do
it 'raises error' do
expect { subject.execute }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://172.16.0.0/file.txt' is blocked: Requests to the local network are not allowed"
+ "URL is blocked: Requests to the local network are not allowed"
)
end
@@ -142,7 +142,7 @@ RSpec.describe Gitlab::ImportExport::RemoteStreamUpload do
expect { subject.execute }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://127.0.0.1/file.txt' is blocked: Requests to localhost are not allowed"
+ "URL is blocked: Requests to localhost are not allowed"
)
end
@@ -168,7 +168,7 @@ RSpec.describe Gitlab::ImportExport::RemoteStreamUpload do
it 'raises error' do
expect { subject.execute }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://172.16.0.0/file.txt' is blocked: Requests to the local network are not allowed"
+ "URL is blocked: Requests to the local network are not allowed"
)
end
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::ImportExport::RemoteStreamUpload do
expect { subject.execute }.to raise_error(
Gitlab::HTTP::BlockedUrlError,
- "URL 'http://example.com/file.txt' is blocked: Requests to localhost are not allowed"
+ "URL is blocked: Requests to localhost are not allowed"
)
end
end
diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb
index f7063f2c4f2..5555990b113 100644
--- a/spec/lib/gitlab/octokit/middleware_spec.rb
+++ b/spec/lib/gitlab/octokit/middleware_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
- shared_examples 'Allowed URL' do
+ shared_examples 'Public URL' do
it 'does not raise an error' do
expect(app).to receive(:call).with(env)
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
end
end
- shared_examples 'Blocked URL' do
+ shared_examples 'Local URL' do
it 'raises an error' do
expect { middleware.call(env) }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
end
@@ -24,24 +24,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
context 'when the URL is a public URL' do
let(:env) { { url: 'https://public-url.com' } }
- it_behaves_like 'Allowed URL'
-
- context 'with failed address check' do
- before do
- stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
- allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError)
- end
-
- it_behaves_like 'Blocked URL'
-
- context 'with disabled dns rebinding check' do
- before do
- stub_application_setting(dns_rebinding_protection_enabled: false)
- end
-
- it_behaves_like 'Allowed URL'
- end
- end
+ it_behaves_like 'Public URL'
end
context 'when the URL is a localhost address' do
@@ -52,7 +35,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
end
- it_behaves_like 'Blocked URL'
+ it_behaves_like 'Local URL'
end
context 'when localhost requests are allowed' do
@@ -60,7 +43,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
end
- it_behaves_like 'Allowed URL'
+ it_behaves_like 'Public URL'
end
end
@@ -72,7 +55,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
end
- it_behaves_like 'Blocked URL'
+ it_behaves_like 'Local URL'
end
context 'when local network requests are allowed' do
@@ -80,7 +63,7 @@ RSpec.describe Gitlab::Octokit::Middleware, feature_category: :importers do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: true)
end
- it_behaves_like 'Allowed URL'
+ it_behaves_like 'Public URL'
end
end
diff --git a/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb b/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb
index e3706a4b106..f09fa3548f8 100644
--- a/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/validate_query_spec.rb
@@ -43,10 +43,7 @@ RSpec.describe Gitlab::Prometheus::Queries::ValidateQuery do
context 'Gitlab::HTTP::BlockedUrlError' do
let(:api_url) { 'http://192.168.1.1' }
- let(:message) do
- "URL 'http://192.168.1.1/api/v1/query?query=avg%28metric%29&time=#{Time.now.to_f}'" \
- " is blocked: Requests to the local network are not allowed"
- end
+ let(:message) { "URL is blocked: Requests to the local network are not allowed" }
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 0d037984799..05f7af7606d 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -174,17 +174,6 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
expect { subject }.to raise_error(described_class::BlockedUrlError)
end
-
- context 'with HTTP_PROXY' do
- before do
- allow(Gitlab).to receive(:http_proxy_env?).and_return(true)
- end
-
- it_behaves_like 'validates URI and hostname' do
- let(:expected_uri) { import_url }
- let(:expected_hostname) { nil }
- end
- end
end
context 'when domain is too long' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 6836d1b51ef..abd29a12b47 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1922,62 +1922,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
end
- describe '#failed_but_allowed?' do
- subject { build.failed_but_allowed? }
-
- context 'when build is not allowed to fail' do
- before do
- build.allow_failure = false
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when build is allowed to fail' do
- before do
- build.allow_failure = true
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when build is a manual action' do
- before do
- build.status = 'manual'
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
describe 'flags' do
describe '#cancelable?' do
subject { build }
@@ -5198,52 +5142,42 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
subject { build.debug_mode? }
context 'when CI_DEBUG_TRACE=true is in variables' do
- context 'when in instance variables' do
- before do
- create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true')
+ ['true', 1, 'y'].each do |value|
+ it 'reflects instance variables' do
+ create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: value)
+
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects group variables' do
+ create(:ci_group_variable, key: 'CI_DEBUG_TRACE', value: value, group: project.group)
- context 'when in group variables' do
- before do
- create(:ci_group_variable, key: 'CI_DEBUG_TRACE', value: 'true', group: project.group)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects pipeline variables' do
+ create(:ci_pipeline_variable, key: 'CI_DEBUG_TRACE', value: value, pipeline: pipeline)
- context 'when in pipeline variables' do
- before do
- create(:ci_pipeline_variable, key: 'CI_DEBUG_TRACE', value: 'true', pipeline: pipeline)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects project variables' do
+ create(:ci_variable, key: 'CI_DEBUG_TRACE', value: value, project: project)
- context 'when in project variables' do
- before do
- create(:ci_variable, key: 'CI_DEBUG_TRACE', value: 'true', project: project)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects job variables' do
+ create(:ci_job_variable, key: 'CI_DEBUG_TRACE', value: value, job: build)
- context 'when in job variables' do
- before do
- create(:ci_job_variable, key: 'CI_DEBUG_TRACE', value: 'true', job: build)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'when in yaml variables' do
+ build.update!(yaml_variables: [{ key: :CI_DEBUG_TRACE, value: value.to_s }])
- context 'when in yaml variables' do
- before do
- build.update!(yaml_variables: [{ key: :CI_DEBUG_TRACE, value: 'true' }])
+ is_expected.to eq true
end
-
- it { is_expected.to eq true }
end
end
@@ -5252,58 +5186,64 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when CI_DEBUG_SERVICES=true is in variables' do
- context 'when in instance variables' do
- before do
- create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: 'true')
+ ['true', 1, 'y'].each do |value|
+ it 'reflects instance variables' do
+ create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: value)
+
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects group variables' do
+ create(:ci_group_variable, key: 'CI_DEBUG_SERVICES', value: value, group: project.group)
- context 'when in group variables' do
- before do
- create(:ci_group_variable, key: 'CI_DEBUG_SERVICES', value: 'true', group: project.group)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects pipeline variables' do
+ create(:ci_pipeline_variable, key: 'CI_DEBUG_SERVICES', value: value, pipeline: pipeline)
- context 'when in pipeline variables' do
- before do
- create(:ci_pipeline_variable, key: 'CI_DEBUG_SERVICES', value: 'true', pipeline: pipeline)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects project variables' do
+ create(:ci_variable, key: 'CI_DEBUG_SERVICES', value: value, project: project)
- context 'when in project variables' do
- before do
- create(:ci_variable, key: 'CI_DEBUG_SERVICES', value: 'true', project: project)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'reflects job variables' do
+ create(:ci_job_variable, key: 'CI_DEBUG_SERVICES', value: value, job: build)
- context 'when in job variables' do
- before do
- create(:ci_job_variable, key: 'CI_DEBUG_SERVICES', value: 'true', job: build)
+ is_expected.to eq true
end
- it { is_expected.to eq true }
- end
+ it 'when in yaml variables' do
+ build.update!(yaml_variables: [{ key: :CI_DEBUG_SERVICES, value: value.to_s }])
- context 'when in yaml variables' do
- before do
- build.update!(yaml_variables: [{ key: :CI_DEBUG_SERVICES, value: 'true' }])
+ is_expected.to eq true
end
-
- it { is_expected.to eq true }
end
end
context 'when CI_DEBUG_SERVICES is not in variables' do
it { is_expected.to eq false }
end
+
+ context 'when metadata has debug_trace_enabled true' do
+ before do
+ build.metadata.update!(debug_trace_enabled: true)
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when metadata has debug_trace_enabled false' do
+ before do
+ build.metadata.update!(debug_trace_enabled: false)
+ end
+
+ it { is_expected.to eq false }
+ end
end
describe '#drop_with_exit_code!' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 4ff451af9de..da87951d9ef 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -422,29 +422,6 @@ RSpec.describe CommitStatus do
end
end
- describe '.exclude_ignored' do
- subject { described_class.exclude_ignored.order(:id) }
-
- let(:statuses) do
- [create_status(when: 'manual', status: 'skipped'),
- create_status(when: 'manual', status: 'success'),
- create_status(when: 'manual', status: 'failed'),
- create_status(when: 'on_failure', status: 'skipped'),
- create_status(when: 'on_failure', status: 'success'),
- create_status(when: 'on_failure', status: 'failed'),
- create_status(allow_failure: true, status: 'success'),
- create_status(allow_failure: true, status: 'failed'),
- create_status(allow_failure: false, status: 'success'),
- create_status(allow_failure: false, status: 'failed'),
- create_status(allow_failure: true, status: 'manual'),
- create_status(allow_failure: false, status: 'manual')]
- end
-
- it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
- end
- end
-
describe '.failed_but_allowed' do
subject { described_class.failed_but_allowed.order(:id) }
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 7af96c7025a..a247881899f 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integration do
+RSpec.describe Integration, feature_category: :integrations do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group) }
@@ -854,6 +854,7 @@ RSpec.describe Integration do
{ name: 'api_key', type: 'password' },
{ name: 'password', type: 'password' },
{ name: 'password_field', type: 'password' },
+ { name: 'webhook' },
{ name: 'some_safe_field' },
{ name: 'safe_field' },
{ name: 'url' },
@@ -881,6 +882,7 @@ RSpec.describe Integration do
field :api_key, type: 'password'
field :password, type: 'password'
field :password_field, type: 'password'
+ field :webhook
field :some_safe_field
field :safe_field
field :url
@@ -1092,6 +1094,8 @@ RSpec.describe Integration do
field :bar, type: 'password'
field :password
+ field :webhook
+
field :with_help, help: -> { 'help' }
field :select, type: 'select'
field :boolean, type: 'checkbox'
@@ -1142,7 +1146,7 @@ RSpec.describe Integration do
it 'registers fields in the fields list' do
expect(integration.fields.pluck(:name)).to match_array %w[
- foo foo_p foo_dt bar password with_help select boolean
+ foo foo_p foo_dt bar password with_help select boolean webhook
]
expect(integration.api_field_names).to match_array %w[
@@ -1157,6 +1161,7 @@ RSpec.describe Integration do
have_attributes(name: 'foo_dt', type: 'text'),
have_attributes(name: 'bar', type: 'password'),
have_attributes(name: 'password', type: 'password'),
+ have_attributes(name: 'webhook', type: 'text'),
have_attributes(name: 'with_help', help: 'help'),
have_attributes(name: 'select', type: 'select'),
have_attributes(name: 'boolean', type: 'checkbox')
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index 65ecd9bee83..2d1e23b103f 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -3,7 +3,7 @@ require 'securerandom'
require 'spec_helper'
-RSpec.describe Integrations::Datadog do
+RSpec.describe Integrations::Datadog, feature_category: :integrations do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/models/integrations/prometheus_spec.rb b/spec/models/integrations/prometheus_spec.rb
index 3c3850854b3..aa248abd3bb 100644
--- a/spec/models/integrations/prometheus_spec.rb
+++ b/spec/models/integrations/prometheus_spec.rb
@@ -239,6 +239,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
context 'behind IAP' do
let(:manual_configuration) { true }
+ let(:google_iap_service_account_json) { Gitlab::Json.generate(google_iap_service_account) }
let(:google_iap_service_account) do
{
@@ -259,7 +260,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
end
def stub_iap_request
- integration.google_iap_service_account_json = Gitlab::Json.generate(google_iap_service_account)
+ integration.google_iap_service_account_json = google_iap_service_account_json
integration.google_iap_audience_client_id = 'IAP_CLIENT_ID.apps.googleusercontent.com'
stub_request(:post, 'https://oauth2.googleapis.com/token')
@@ -278,6 +279,17 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
expect(integration.prometheus_client.send(:options)[:headers]).to eq(authorization: "Bearer FOO")
end
+ context 'with invalid IAP JSON' do
+ let(:google_iap_service_account_json) { 'invalid json' }
+
+ it 'does not include authorization header' do
+ stub_iap_request
+
+ expect(integration.prometheus_client).not_to be_nil
+ expect(integration.prometheus_client.send(:options)).not_to have_key(:headers)
+ end
+ end
+
context 'when passed with token_credential_uri', issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/284819' do
let(:malicious_host) { 'http://example.com' }
@@ -477,4 +489,45 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
end
end
end
+
+ describe '#google_iap_service_account_json' do
+ subject(:iap_details) { integration.google_iap_service_account_json }
+
+ before do
+ integration.google_iap_service_account_json = value
+ end
+
+ context 'with valid JSON' do
+ let(:masked_value) { described_class::MASKED_VALUE }
+ let(:json) { Gitlab::Json.parse(iap_details) }
+
+ let(:value) do
+ Gitlab::Json.generate({
+ type: 'service_account',
+ private_key: 'SECRET',
+ foo: 'secret',
+ nested: {
+ key: 'value'
+ }
+ })
+ end
+
+ it 'masks all JSON values', issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384580' do
+ expect(json).to eq(
+ 'type' => masked_value,
+ 'private_key' => masked_value,
+ 'foo' => masked_value,
+ 'nested' => masked_value
+ )
+ end
+ end
+
+ context 'with invalid JSON' do
+ where(:value) { [nil, '', ' ', 'invalid json'] }
+
+ with_them do
+ it { is_expected.to eq(value) }
+ end
+ end
+ end
end
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 10dd9c3b556..8b3ec59b785 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -750,11 +750,7 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
end
- context 'when ci_debug_trace is set to true' do
- before_all do
- create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: true)
- end
-
+ shared_examples_for "additional access criteria" do
where(:public_builds, :user_project_role, :expected_status) do
true | 'developer' | :ok
true | 'guest' | :forbidden
@@ -776,30 +772,28 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
end
end
+ describe 'when metadata debug_trace_enabled is set to true' do
+ before do
+ job.metadata.update!(debug_trace_enabled: true)
+ end
+
+ it_behaves_like "additional access criteria"
+ end
+
+ context 'when ci_debug_trace is set to true' do
+ before_all do
+ create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: true)
+ end
+
+ it_behaves_like "additional access criteria"
+ end
+
context 'when ci_debug_services is set to true' do
before_all do
create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: true)
end
- where(:public_builds, :user_project_role, :expected_status) do
- true | 'developer' | :ok
- true | 'guest' | :forbidden
- false | 'developer' | :ok
- false | 'guest' | :forbidden
- end
-
- with_them do
- before do
- project.update!(public_builds: public_builds)
- project.add_role(user, user_project_role)
-
- get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
- end
-
- it 'renders successfully to authorized users' do
- expect(response).to have_gitlab_http_status(expected_status)
- end
- end
+ it_behaves_like "additional access criteria"
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index e474070f621..57050c39083 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -249,6 +249,18 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
end
end
+ context 'when per_page is over 100' do
+ let(:per_page) { 101 }
+
+ it 'returns 100 commits (maximum)' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(
+ hash_including(ref: ref_name, limit: 100, offset: 0)
+ )
+
+ request
+ end
+ end
+
context 'when pagination params are invalid' do
let_it_be(:project) { create(:project, :repository) }
@@ -279,7 +291,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
where(:page, :per_page, :error_message, :status) do
0 | nil | nil | :success
- -10 | nil | nil | :internal_server_error
+ -10 | nil | nil | :success
'a' | nil | 'page is invalid' | :bad_request
nil | 0 | 'per_page has a value not allowed' | :bad_request
nil | -1 | nil | :success
@@ -297,6 +309,18 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
end
end
end
+
+ context 'when per_page is below 0' do
+ let(:per_page) { -100 }
+
+ it 'returns 20 commits (default)' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(
+ hash_including(ref: ref_name, limit: 20, offset: 0)
+ )
+
+ request
+ end
+ end
end
end
end
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 462cc1e3b5d..4b388304621 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -377,6 +377,15 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
expect(response).to match_response_schema('release/link')
end
+ context 'when params are invalid' do
+ it 'returns 400 error' do
+ put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer),
+ params: params.merge(url: 'wrong_url')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when using `direct_asset_path`' do
it 'updates the release link' do
put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer),
@@ -534,6 +543,21 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
end
+ context 'when destroy process fails' do
+ before do
+ allow_next_instance_of(::Releases::Links::DestroyService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it_behaves_like '400 response' do
+ let(:message) { 'error' }
+ let(:request) do
+ delete api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer)
+ end
+ end
+ end
+
context 'when there are no corresponding release link' do
let!(:release_link) {}
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index b02c7135b7b..ab5e04246e8 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -111,6 +111,22 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
let(:project) { create(:project, :public, :repository) }
it_behaves_like 'repository tags'
+
+ context 'and releases are private' do
+ before do
+ create(:release, project: project, tag: tag_name)
+ project.project_feature.update!(releases_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns the repository tags without release information' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tags')
+ expect(response).to include_pagination_headers
+ expect(json_response.map { |r| r.has_key?('release') }).to all(be_falsey)
+ end
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -251,6 +267,21 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
it_behaves_like "cache expired"
end
+
+ context 'when user is not allowed to :read_release' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(releases_access_level: ProjectFeature::PRIVATE)
+
+ get api(route, user) # Cache as a user allowed to :read_release
+ end
+
+ it "isn't cached" do
+ expect(API::Entities::Tag).to receive(:represent).exactly(3).times
+
+ get api(route, nil)
+ end
+ end
end
context 'when gitaly is unavailable' do
@@ -302,6 +333,21 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
let(:project) { create(:project, :public, :repository) }
it_behaves_like 'repository tag'
+
+ context 'and releases are private' do
+ before do
+ create(:release, project: project, tag: tag_name)
+ project.project_feature.update!(releases_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns the repository tags without release information' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tag')
+ expect(json_response.has_key?('release')).to be_falsey
+ end
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -328,6 +374,24 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
let(:request) { get api(route, guest) }
end
end
+
+ context 'with releases' do
+ let(:description) { 'Awesome release!' }
+
+ before do
+ create(:release, project: project, tag: tag_name, description: description)
+ end
+
+ it 'returns release information' do
+ get api(route, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/tag')
+
+ expect(json_response['message']).to eq(tag_message)
+ expect(json_response.dig('release', 'description')).to eq(description)
+ end
+ end
end
describe 'POST /projects/:id/repository/tags' do
diff --git a/spec/services/releases/links/create_service_spec.rb b/spec/services/releases/links/create_service_spec.rb
new file mode 100644
index 00000000000..aa154647509
--- /dev/null
+++ b/spec/services/releases/links/create_service_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::Links::CreateService, feature_category: :release_orchestration do
+ let(:service) { described_class.new(release, user, params) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }
+
+ let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type } }
+ let(:name) { 'link' }
+ let(:url) { 'https://example.com' }
+ let(:direct_asset_path) { '/path' }
+ let(:link_type) { 'other' }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute }
+
+ let(:link) { subject.payload[:link] }
+
+ it 'successfully creates a release link' do
+ expect { execute }.to change { Releases::Link.count }.by(1)
+
+ expect(link).to have_attributes(
+ name: name,
+ url: url,
+ filepath: direct_asset_path,
+ link_type: link_type
+ )
+ end
+
+ context 'when user does not have access to create release link' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns an error' do
+ expect { execute }.not_to change { Releases::Link.count }
+
+ is_expected.to be_error
+ expect(execute.message).to include('Access Denied')
+ end
+ end
+
+ context 'when url is invalid' do
+ let(:url) { 'not_a_url' }
+
+ it 'returns an error' do
+ expect { execute }.not_to change { Releases::Link.count }
+
+ is_expected.to be_error
+ expect(execute.message[0]).to include('Url is blocked')
+ end
+ end
+
+ context 'when both direct_asset_path and filepath are provided' do
+ let(:params) { super().merge(filepath: '/filepath') }
+
+ it 'prefers direct_asset_path' do
+ is_expected.to be_success
+
+ expect(link.filepath).to eq(direct_asset_path)
+ end
+ end
+
+ context 'when only filepath is set' do
+ let(:params) { super().merge(filepath: '/filepath') }
+ let(:direct_asset_path) { nil }
+
+ it 'uses filepath' do
+ is_expected.to be_success
+
+ expect(link.filepath).to eq('/filepath')
+ end
+ end
+ end
+end
diff --git a/spec/services/releases/links/destroy_service_spec.rb b/spec/services/releases/links/destroy_service_spec.rb
new file mode 100644
index 00000000000..fed98a62aa7
--- /dev/null
+++ b/spec/services/releases/links/destroy_service_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::Links::DestroyService, feature_category: :release_orchestration do
+ let(:service) { described_class.new(release, user, {}) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }
+
+ let!(:release_link) do
+ create(
+ :release_link,
+ release: release,
+ name: 'awesome-app.dmg',
+ url: 'https://example.com/download/awesome-app.dmg'
+ )
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute(release_link) }
+
+ it 'successfully deletes a release link' do
+ expect { execute }.to change { release.links.count }.by(-1)
+
+ is_expected.to be_success
+ end
+
+ context 'when user does not have access to delete release link' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns an error' do
+ expect { execute }.not_to change { release.links.count }
+
+ is_expected.to be_error
+ expect(execute.message).to include('Access Denied')
+ end
+ end
+
+ context 'when release link does not exist' do
+ let(:release_link) { nil }
+
+ it 'returns an error' do
+ expect { execute }.not_to change { release.links.count }
+
+ is_expected.to be_error
+ expect(execute.message).to eq('Link does not exist')
+ end
+ end
+
+ context 'when release link deletion failed' do
+ before do
+ allow(release_link).to receive(:destroy).and_return(false)
+ end
+
+ it 'returns an error' do
+ expect { execute }.not_to change { release.links.count }
+
+ is_expected.to be_error
+ end
+ end
+ end
+end
diff --git a/spec/services/releases/links/update_service_spec.rb b/spec/services/releases/links/update_service_spec.rb
new file mode 100644
index 00000000000..40756c7eced
--- /dev/null
+++ b/spec/services/releases/links/update_service_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::Links::UpdateService, feature_category: :release_orchestration do
+ let(:service) { described_class.new(release, user, params) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }
+
+ let(:release_link) do
+ create(
+ :release_link,
+ release: release,
+ name: 'awesome-app.dmg',
+ url: 'https://example.com/download/awesome-app.dmg'
+ )
+ end
+
+ let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type } }
+ let(:name) { 'link' }
+ let(:url) { 'https://example.com' }
+ let(:direct_asset_path) { '/path' }
+ let(:link_type) { 'other' }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject(:execute) { service.execute(release_link) }
+
+ let(:updated_link) { execute.payload[:link] }
+
+ it 'successfully updates a release link' do
+ is_expected.to be_success
+
+ expect(updated_link).to have_attributes(
+ name: name,
+ url: url,
+ filepath: direct_asset_path,
+ link_type: link_type
+ )
+ end
+
+ context 'when user does not have access to update release link' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns an error' do
+ is_expected.to be_error
+ expect(execute.message).to include('Access Denied')
+ end
+ end
+
+ context 'when url is invalid' do
+ let(:url) { 'not_a_url' }
+
+ it 'returns an error' do
+ is_expected.to be_error
+ expect(execute.message[0]).to include('Url is blocked')
+ end
+ end
+
+ context 'when both direct_asset_path and filepath are provided' do
+ let(:params) { super().merge(filepath: '/filepath') }
+
+ it 'prefers direct_asset_path' do
+ is_expected.to be_success
+
+ expect(updated_link.filepath).to eq(direct_asset_path)
+ end
+ end
+
+ context 'when only filepath is set' do
+ let(:params) { super().merge(filepath: '/filepath') }
+ let(:direct_asset_path) { nil }
+
+ it 'uses filepath' do
+ is_expected.to be_success
+
+ expect(updated_link.filepath).to eq('/filepath')
+ end
+ end
+ end
+end
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
index f0eae9554dc..d81bbc30318 100644
--- a/spec/services/resource_access_tokens/create_service_spec.rb
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -27,6 +27,13 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
end
end
+ shared_examples 'correct error message' do
+ it 'returns correct error message' do
+ expect(subject.error?).to be true
+ expect(subject.errors).to include(error_message)
+ end
+ end
+
shared_examples 'allows creation of bot with valid params' do
it { expect { subject }.to change { User.count }.by(1) }
@@ -215,16 +222,11 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
end
context 'when invalid scope is passed' do
+ let(:error_message) { 'Scopes can only contain available scopes' }
let_it_be(:params) { { scopes: [:invalid_scope] } }
it_behaves_like 'token creation fails'
-
- it 'returns the scope error message' do
- response = subject
-
- expect(response.error?).to be true
- expect(response.errors).to include("Scopes can only contain available scopes")
- end
+ it_behaves_like 'correct error message'
end
end
@@ -232,6 +234,7 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
let_it_be(:bot_user) { create(:user, :project_bot) }
let(:unpersisted_member) { build(:project_member, source: resource, user: bot_user) }
+ let(:error_message) { 'Could not provision maintainer access to project access token' }
before do
allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
@@ -241,13 +244,7 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
end
it_behaves_like 'token creation fails'
-
- it 'returns the provisioning error message' do
- response = subject
-
- expect(response.error?).to be true
- expect(response.errors).to include("Could not provision maintainer access to project access token")
- end
+ it_behaves_like 'correct error message'
end
end
@@ -261,14 +258,10 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
end
shared_examples 'when user does not have permission to create a resource bot' do
+ let(:error_message) { "User does not have permission to create #{resource_type} access token" }
+
it_behaves_like 'token creation fails'
-
- it 'returns the permission error message' do
- response = subject
-
- expect(response.error?).to be true
- expect(response.errors).to include("User does not have permission to create #{resource_type} access token")
- end
+ it_behaves_like 'correct error message'
end
context 'when resource is a project' do
@@ -288,11 +281,19 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
let_it_be(:params) { { access_level: Gitlab::Access::OWNER } }
context 'when the executor is a MAINTAINER' do
- it 'does not add the bot user with the specified access level in the resource' do
- response = subject
+ let(:error_message) { 'Could not provision owner access to project access token' }
- expect(response.error?).to be true
- expect(response.errors).to include('Could not provision owner access to project access token')
+ context 'with OWNER access_level, in integer format' do
+ it_behaves_like 'token creation fails'
+ it_behaves_like 'correct error message'
+ end
+
+ context 'with OWNER access_level, in string format' do
+ let(:error_message) { 'Could not provision owner access to project access token' }
+ let_it_be(:params) { { access_level: Gitlab::Access::OWNER.to_s } }
+
+ it_behaves_like 'token creation fails'
+ it_behaves_like 'correct error message'
end
end
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index f07e4f6c9c3..9612b657093 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -99,6 +99,7 @@ Integration.available_integration_names.each do |integration|
def initialize_integration(integration, attrs = {})
record = project.find_or_initialize_integration(integration)
+ record.reset_updated_properties if integration == 'datadog'
record.attributes = attrs
record.properties = integration_attrs
record.save!
diff --git a/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb b/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb
index 553e9f10b0d..cef76bd4356 100644
--- a/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb
+++ b/spec/support/shared_examples/initializers/uses_gitlab_url_blocker_shared_examples.rb
@@ -33,7 +33,7 @@ RSpec.shared_examples 'a request using Gitlab::UrlBlocker' do
expect { make_request('https://example.com') }
.to raise_error(url_blocked_error_class,
- "URL 'https://example.com' is blocked: Requests to the local network are not allowed")
+ "URL is blocked: Requests to the local network are not allowed")
end
it 'raises error when it is a request that resolves to a localhost address' do
@@ -41,19 +41,19 @@ RSpec.shared_examples 'a request using Gitlab::UrlBlocker' do
expect { make_request('https://example.com') }
.to raise_error(url_blocked_error_class,
- "URL 'https://example.com' is blocked: Requests to localhost are not allowed")
+ "URL is blocked: Requests to localhost are not allowed")
end
it 'raises error when it is a request to local address' do
expect { make_request('http://172.16.0.0') }
.to raise_error(url_blocked_error_class,
- "URL 'http://172.16.0.0' is blocked: Requests to the local network are not allowed")
+ "URL is blocked: Requests to the local network are not allowed")
end
it 'raises error when it is a request to localhost address' do
expect { make_request('http://127.0.0.1') }
.to raise_error(url_blocked_error_class,
- "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed")
+ "URL is blocked: Requests to localhost are not allowed")
end
end
@@ -69,13 +69,13 @@ RSpec.shared_examples 'a request using Gitlab::UrlBlocker' do
it 'raises error when it is a request to local address' do
expect { make_request('https://172.16.0.0:8080') }
.to raise_error(url_blocked_error_class,
- "URL 'https://172.16.0.0:8080' is blocked: Requests to the local network are not allowed")
+ "URL is blocked: Requests to the local network are not allowed")
end
it 'raises error when it is a request to localhost address' do
expect { make_request('https://127.0.0.1:8080') }
.to raise_error(url_blocked_error_class,
- "URL 'https://127.0.0.1:8080' is blocked: Requests to localhost are not allowed")
+ "URL is blocked: Requests to localhost are not allowed")
end
end
diff --git a/tooling/lib/tooling/find_codeowners.rb b/tooling/lib/tooling/find_codeowners.rb
index cc37d4db1ec..e542ab9967c 100644
--- a/tooling/lib/tooling/find_codeowners.rb
+++ b/tooling/lib/tooling/find_codeowners.rb
@@ -48,11 +48,7 @@ module Tooling
def load_config
config_path = "#{__dir__}/../../config/CODEOWNERS.yml"
- if YAML.respond_to?(:safe_load_file) # Ruby 3.0+
- YAML.safe_load_file(config_path, symbolize_names: true)
- else
- YAML.safe_load(File.read(config_path), symbolize_names: true)
- end
+ YAML.safe_load_file(config_path, symbolize_names: true)
end
# Copied and modified from ee/lib/gitlab/code_owners/file.rb