New upstream version 15.3.2+ds1
This commit is contained in:
parent
761ace024e
commit
9072983091
82 changed files with 1496 additions and 635 deletions
|
@ -726,6 +726,7 @@ Gitlab/NamespacedClass:
|
|||
- 'app/validators/top_level_group_validator.rb'
|
||||
- 'app/validators/untrusted_regexp_validator.rb'
|
||||
- 'app/validators/x509_certificate_credentials_validator.rb'
|
||||
- 'app/validators/bytesize_validator.rb'
|
||||
- 'app/workers/admin_email_worker.rb'
|
||||
- 'app/workers/approve_blocked_pending_approval_users_worker.rb'
|
||||
- 'app/workers/archive_trace_worker.rb'
|
||||
|
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -2,6 +2,28 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 15.3.2 (2022-08-30)
|
||||
|
||||
### Security (17 changes)
|
||||
|
||||
- [No overriding methods for Sawyer class](gitlab-org/security/gitlab@397aa9e269676f4ab3dfba4c3ba8fef131b5b4bd) ([merge request](gitlab-org/security/gitlab!2754))
|
||||
- [Update Oj to v3.13.21](gitlab-org/security/gitlab@15f86c00b579ad1b4aeedd395f9239e8229c6f8b) ([merge request](gitlab-org/security/gitlab!2730))
|
||||
- [Prevent long loops when generating suggested branch name](gitlab-org/security/gitlab@1479c9e2a0444794ea274b07e0f59e8a50ced6ee) ([merge request](gitlab-org/security/gitlab!2743))
|
||||
- [IDOR in Zentao integration issue show page](gitlab-org/security/gitlab@92fdf89045bf294d4ee0338ba3f26c91094a073e) ([merge request](gitlab-org/security/gitlab!2740))
|
||||
- [Patch VULNDB-255039 (potential Rack cache poisoning)](gitlab-org/security/gitlab@383c926cc8aa4e2c4273556a181e1ddc1b71049f) ([merge request](gitlab-org/security/gitlab!2697))
|
||||
- [HTML escape the label background color](gitlab-org/security/gitlab@1e43656560fbc13907af72d5d4f696df95d7f49c) ([merge request](gitlab-org/security/gitlab!2719))
|
||||
- [Sandbox jupyter notebook HTML output](gitlab-org/security/gitlab@3ade5f2fadbb0c15d9e5a14306d0a79136a8f23e) ([merge request](gitlab-org/security/gitlab!2710))
|
||||
- [Fix unauthorized GFM references in Incident Timeline](gitlab-org/security/gitlab@2e18b59472b5a43921d39433e60038b0f254d123) ([merge request](gitlab-org/security/gitlab!2707))
|
||||
- [Optimize handling repositories with huge trees](gitlab-org/security/gitlab@4bfaca71c8d8f663242138049cf5639e69326bbb) ([merge request](gitlab-org/security/gitlab!2706))
|
||||
- [Parse commit trailers without using regexp](gitlab-org/security/gitlab@c15b2cd9b5e572a9bbc7c0c5cb7c9511f1a04ead) ([merge request](gitlab-org/security/gitlab!2699))
|
||||
- [Check for pathological markdown input](gitlab-org/security/gitlab@2fd5e1133e1acd82cdb524f059b554976cd68f51) ([merge request](gitlab-org/security/gitlab!2733))
|
||||
- [Replaced smooshpack to fix the vulnerability in LivePreview](gitlab-org/security/gitlab@114637f8f0d9add00914ac3e4562419b0f1b4f63) ([merge request](gitlab-org/security/gitlab!2739))
|
||||
- [Update package auth for group IP allowlist](gitlab-org/security/gitlab@7e830349a8425dbab65ce92d3e8ebd0afa734381) ([merge request](gitlab-org/security/gitlab!2686))
|
||||
- [Don't show pipeline status](gitlab-org/security/gitlab@1b5fbb9bcb4dde12a2af075e45407cbc6109494d) ([merge request](gitlab-org/security/gitlab!2712))
|
||||
- [Sanitize img attributes in Banzai::Filter::ImageLinkFilter](gitlab-org/security/gitlab@22ece3568d6b3aed305ed97aab9fdbb22ca068e8) ([merge request](gitlab-org/security/gitlab!2722))
|
||||
- [Validate description length for snippets](gitlab-org/security/gitlab@24592d39d7b8956a0e712026e5b988a82d37e771) ([merge request](gitlab-org/security/gitlab!2702))
|
||||
- [Prevent brute force vuln for Git over HTTP(S) requests](gitlab-org/security/gitlab@fcff307eff525d15e835e65e0e3e3a2395f0b840) ([merge request](gitlab-org/security/gitlab!2716))
|
||||
|
||||
## 15.3.1 (2022-08-22)
|
||||
|
||||
### Security (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
15.3.1
|
||||
15.3.2
|
2
Gemfile
2
Gemfile
|
@ -533,7 +533,7 @@ gem 'valid_email', '~> 0.1'
|
|||
# JSON
|
||||
gem 'json', '~> 2.5.1'
|
||||
gem 'json_schemer', '~> 0.2.18'
|
||||
gem 'oj', '~> 3.13.20'
|
||||
gem 'oj', '~> 3.13.21'
|
||||
gem 'multi_json', '~> 1.14.1'
|
||||
gem 'yajl-ruby', '~> 1.4.3', require: 'yajl'
|
||||
|
||||
|
|
|
@ -887,7 +887,7 @@ GEM
|
|||
plist (~> 3.1)
|
||||
train-core
|
||||
wmi-lite (~> 1.0)
|
||||
oj (3.13.20)
|
||||
oj (3.13.21)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -1651,7 +1651,7 @@ DEPENDENCIES
|
|||
oauth2 (~> 2.0)
|
||||
octokit (~> 4.15)
|
||||
ohai (~> 16.10)
|
||||
oj (~> 3.13.20)
|
||||
oj (~> 3.13.21)
|
||||
omniauth (~> 1.8)
|
||||
omniauth-alicloud (~> 1.0.1)
|
||||
omniauth-atlassian-oauth2 (~> 0.2.0)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
15.3.1
|
||||
15.3.2
|
|
@ -2,7 +2,7 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { listen } from 'codesandbox-api';
|
||||
import { isEmpty, debounce } from 'lodash';
|
||||
import { Manager } from 'smooshpack';
|
||||
import { SandpackClient } from '@codesandbox/sandpack-client';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import {
|
||||
packageJsonPath,
|
||||
|
@ -21,7 +21,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
manager: {},
|
||||
client: {},
|
||||
loading: false,
|
||||
sandpackReady: false,
|
||||
};
|
||||
|
@ -94,11 +94,11 @@ export default {
|
|||
this.sandpackReady = false;
|
||||
eventHub.$off('ide.files.change', this.onFilesChangeCallback);
|
||||
|
||||
if (!isEmpty(this.manager)) {
|
||||
this.manager.listener();
|
||||
if (!isEmpty(this.client)) {
|
||||
this.client.cleanup();
|
||||
}
|
||||
|
||||
this.manager = {};
|
||||
this.client = {};
|
||||
|
||||
if (this.listener) {
|
||||
this.listener();
|
||||
|
@ -120,7 +120,7 @@ export default {
|
|||
return this.loadFileContent(this.mainEntry)
|
||||
.then(() => this.$nextTick())
|
||||
.then(() => {
|
||||
this.initManager();
|
||||
this.initClient();
|
||||
|
||||
this.listener = listen((e) => {
|
||||
switch (e.type) {
|
||||
|
@ -136,15 +136,15 @@ export default {
|
|||
update() {
|
||||
if (!this.sandpackReady) return;
|
||||
|
||||
if (isEmpty(this.manager)) {
|
||||
if (isEmpty(this.client)) {
|
||||
this.initPreview();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.manager.updatePreview(this.sandboxOpts);
|
||||
this.client.updatePreview(this.sandboxOpts);
|
||||
},
|
||||
initManager() {
|
||||
initClient() {
|
||||
const { codesandboxBundlerUrl: bundlerURL } = this;
|
||||
|
||||
const settings = {
|
||||
|
@ -155,7 +155,7 @@ export default {
|
|||
...(bundlerURL ? { bundlerURL } : {}),
|
||||
};
|
||||
|
||||
this.manager = new Manager('#ide-preview', this.sandboxOpts, settings);
|
||||
this.client = new SandpackClient('#ide-preview', this.sandboxOpts, settings);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -164,7 +164,7 @@ export default {
|
|||
<template>
|
||||
<div class="preview h-100 w-100 d-flex flex-column gl-bg-white">
|
||||
<template v-if="showPreview">
|
||||
<navigator :manager="manager" />
|
||||
<navigator :client="client" />
|
||||
<div id="ide-preview"></div>
|
||||
</template>
|
||||
<div
|
||||
|
|
|
@ -8,7 +8,7 @@ export default {
|
|||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
manager: {
|
||||
client: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
@ -51,7 +51,7 @@ export default {
|
|||
onUrlChange(e) {
|
||||
const lastPath = this.path;
|
||||
|
||||
this.path = e.url.replace(this.manager.bundlerURL, '') || '/';
|
||||
this.path = e.url.replace(this.client.bundlerURL, '') || '/';
|
||||
|
||||
if (lastPath !== this.path) {
|
||||
this.currentBrowsingIndex =
|
||||
|
@ -79,7 +79,7 @@ export default {
|
|||
},
|
||||
visitPath(path) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
|
||||
this.client.iframe.src = `${this.client.bundlerURL}${path}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,6 +40,13 @@ export default {
|
|||
<template>
|
||||
<div class="output">
|
||||
<prompt type="Out" :count="count" :show-output="showOutput" />
|
||||
<div v-safe-html:[$options.safeHtmlConfig]="rawCode" class="gl-overflow-auto"></div>
|
||||
<iframe
|
||||
sandbox
|
||||
:srcdoc="rawCode"
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
width="100%"
|
||||
class="gl-overflow-auto"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -36,31 +36,40 @@ class JwtController < ApplicationController
|
|||
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
|
||||
|
||||
if @authentication_result.failed?
|
||||
render_unauthorized
|
||||
log_authentication_failed(login, @authentication_result)
|
||||
render_access_denied
|
||||
end
|
||||
end
|
||||
rescue Gitlab::Auth::MissingPersonalAccessTokenError
|
||||
render_missing_personal_access_token
|
||||
render_access_denied
|
||||
end
|
||||
|
||||
def render_missing_personal_access_token
|
||||
render json: {
|
||||
errors: [
|
||||
{ code: 'UNAUTHORIZED',
|
||||
message: _('HTTP Basic: Access denied\n' \
|
||||
'You must use a personal access token with \'api\' scope for Git over HTTP.\n' \
|
||||
'You can generate one at %{profile_personal_access_tokens_url}') % { profile_personal_access_tokens_url: profile_personal_access_tokens_url } }
|
||||
]
|
||||
}, status: :unauthorized
|
||||
def log_authentication_failed(login, result)
|
||||
log_info = {
|
||||
message: 'JWT authentication failed',
|
||||
http_user: login,
|
||||
remote_ip: request.ip,
|
||||
auth_service: params[:service],
|
||||
'auth_result.type': result.type,
|
||||
'auth_result.actor_type': result.actor&.class
|
||||
}.merge(::Gitlab::ApplicationContext.current)
|
||||
|
||||
Gitlab::AuthLogger.warn(log_info)
|
||||
end
|
||||
|
||||
def render_unauthorized
|
||||
render json: {
|
||||
errors: [
|
||||
{ code: 'UNAUTHORIZED',
|
||||
message: 'HTTP Basic: Access denied' }
|
||||
]
|
||||
}, status: :unauthorized
|
||||
def render_access_denied
|
||||
help_page = help_page_url(
|
||||
'user/profile/account/two_factor_authentication',
|
||||
anchor: 'troubleshooting'
|
||||
)
|
||||
|
||||
render(
|
||||
json: { errors: [{
|
||||
code: 'UNAUTHORIZED',
|
||||
message: format(_("HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"), help_page_url: help_page)
|
||||
}] },
|
||||
status: :unauthorized
|
||||
)
|
||||
end
|
||||
|
||||
def auth_params
|
||||
|
|
|
@ -67,9 +67,21 @@ module Repositories
|
|||
end
|
||||
|
||||
send_challenges
|
||||
render plain: "HTTP Basic: Access denied\n", status: :unauthorized
|
||||
render_access_denied
|
||||
rescue Gitlab::Auth::MissingPersonalAccessTokenError
|
||||
render_missing_personal_access_token
|
||||
render_access_denied
|
||||
end
|
||||
|
||||
def render_access_denied
|
||||
help_page = help_page_url(
|
||||
'topics/git/troubleshooting_git',
|
||||
anchor: 'error-on-git-fetch-http-basic-access-denied'
|
||||
)
|
||||
|
||||
render(
|
||||
plain: format(_("HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"), help_page_url: help_page),
|
||||
status: :unauthorized
|
||||
)
|
||||
end
|
||||
|
||||
def basic_auth_provided?
|
||||
|
@ -103,13 +115,6 @@ module Repositories
|
|||
@container, @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(repository_path)
|
||||
end
|
||||
|
||||
def render_missing_personal_access_token
|
||||
render plain: "HTTP Basic: Access denied\n" \
|
||||
"You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \
|
||||
"You can generate one at #{profile_personal_access_tokens_url}",
|
||||
status: :unauthorized
|
||||
end
|
||||
|
||||
def repository
|
||||
strong_memoize(:repository) do
|
||||
repo_type.repository_for(container)
|
||||
|
|
|
@ -32,7 +32,11 @@ module Resolvers
|
|||
page_token: cursor
|
||||
}
|
||||
|
||||
tree = repository.tree(args[:ref], args[:path], recursive: args[:recursive], pagination_params: pagination_params)
|
||||
tree = repository.tree(
|
||||
args[:ref], args[:path], recursive: args[:recursive],
|
||||
skip_flat_paths: false,
|
||||
pagination_params: pagination_params
|
||||
)
|
||||
|
||||
next_cursor = tree.cursor&.next_cursor
|
||||
Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree)
|
||||
|
|
|
@ -33,11 +33,6 @@ module Types
|
|||
null: true,
|
||||
description: 'Text note of the timeline event.'
|
||||
|
||||
field :note_html,
|
||||
GraphQL::Types::String,
|
||||
null: true,
|
||||
description: 'HTML note of the timeline event.'
|
||||
|
||||
field :promoted_from_note,
|
||||
Types::Notes::NoteType,
|
||||
null: true,
|
||||
|
@ -67,6 +62,8 @@ module Types
|
|||
Types::TimeType,
|
||||
null: false,
|
||||
description: 'Timestamp when the event updated.'
|
||||
|
||||
markdown_field :note_html, null: true, description: 'HTML note of the timeline event.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,7 +171,7 @@ module CommitsHelper
|
|||
ref,
|
||||
{
|
||||
merge_request: merge_request&.cache_key,
|
||||
pipeline_status: commit.status_for(ref)&.cache_key,
|
||||
pipeline_status: commit.detailed_status_for(ref)&.cache_key,
|
||||
xhr: request.xhr?,
|
||||
controller: controller.controller_path,
|
||||
path: @path # referred to in #link_to_browse_code
|
||||
|
|
|
@ -247,7 +247,7 @@ module LabelsHelper
|
|||
class="#{css_class}"
|
||||
data-container="body"
|
||||
data-html="true"
|
||||
#{"style=\"background-color: #{bg_color}\"" if bg_color}
|
||||
#{"style=\"background-color: #{h bg_color}\"" if bg_color}
|
||||
>#{ERB::Util.html_escape_once(name)}#{suffix}</span>
|
||||
HTML
|
||||
end
|
||||
|
|
|
@ -69,6 +69,10 @@ module Integrations
|
|||
}
|
||||
end
|
||||
|
||||
def client_url
|
||||
api_url.presence || url
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
name.demodulize.downcase
|
||||
end
|
||||
|
|
|
@ -458,7 +458,13 @@ class Issue < ApplicationRecord
|
|||
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
|
||||
|
||||
start_counting_from = 2
|
||||
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
|
||||
|
||||
branch_name_generator = -> (counter) do
|
||||
suffix = counter > 5 ? SecureRandom.hex(8) : counter
|
||||
"#{to_branch_name}-#{suffix}"
|
||||
end
|
||||
|
||||
Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
|
||||
project.repository.branch_exists?(suggested_branch_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -677,24 +677,24 @@ class Repository
|
|||
@head_commit ||= commit(self.root_ref)
|
||||
end
|
||||
|
||||
def head_tree
|
||||
def head_tree(skip_flat_paths: true)
|
||||
if head_commit
|
||||
@head_tree ||= Tree.new(self, head_commit.sha, nil)
|
||||
@head_tree ||= Tree.new(self, head_commit.sha, nil, skip_flat_paths: skip_flat_paths)
|
||||
end
|
||||
end
|
||||
|
||||
def tree(sha = :head, path = nil, recursive: false, pagination_params: nil)
|
||||
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil)
|
||||
if sha == :head
|
||||
return unless head_commit
|
||||
|
||||
if path.nil?
|
||||
return head_tree
|
||||
return head_tree(skip_flat_paths: skip_flat_paths)
|
||||
else
|
||||
sha = head_commit.sha
|
||||
end
|
||||
end
|
||||
|
||||
Tree.new(self, sha, path, recursive: recursive, pagination_params: pagination_params)
|
||||
Tree.new(self, sha, path, recursive: recursive, skip_flat_paths: skip_flat_paths, pagination_params: pagination_params)
|
||||
end
|
||||
|
||||
def blob_at_branch(branch_name, path)
|
||||
|
|
|
@ -22,6 +22,8 @@ class Snippet < ApplicationRecord
|
|||
|
||||
MAX_FILE_COUNT = 10
|
||||
|
||||
DESCRIPTION_LENGTH_MAX = 1.megabyte
|
||||
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
cache_markdown_field :content
|
||||
|
@ -57,19 +59,10 @@ class Snippet < ApplicationRecord
|
|||
validates :title, presence: true, length: { maximum: 255 }
|
||||
validates :file_name,
|
||||
length: { maximum: 255 }
|
||||
validates :description, bytesize: { maximum: -> { DESCRIPTION_LENGTH_MAX } }, if: :description_changed?
|
||||
|
||||
validates :content, presence: true
|
||||
validates :content,
|
||||
length: {
|
||||
maximum: ->(_) { Gitlab::CurrentSettings.snippet_size_limit },
|
||||
message: -> (_, data) do
|
||||
current_value = ActiveSupport::NumberHelper.number_to_human_size(data[:value].size)
|
||||
max_size = ActiveSupport::NumberHelper.number_to_human_size(Gitlab::CurrentSettings.snippet_size_limit)
|
||||
|
||||
_("is too long (%{current_value}). The maximum size is %{max_size}.") % { current_value: current_value, max_size: max_size }
|
||||
end
|
||||
},
|
||||
if: :content_changed?
|
||||
validates :content, bytesize: { maximum: -> { Gitlab::CurrentSettings.snippet_size_limit } }, if: :content_changed?
|
||||
|
||||
after_create :create_statistics
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class Tree
|
|||
|
||||
attr_accessor :repository, :sha, :path, :entries, :cursor
|
||||
|
||||
def initialize(repository, sha, path = '/', recursive: false, pagination_params: nil)
|
||||
def initialize(repository, sha, path = '/', recursive: false, skip_flat_paths: true, pagination_params: nil)
|
||||
path = '/' if path.blank?
|
||||
|
||||
@repository = repository
|
||||
|
@ -14,7 +14,7 @@ class Tree
|
|||
@path = path
|
||||
|
||||
git_repo = @repository.raw_repository
|
||||
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, pagination_params)
|
||||
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, skip_flat_paths, pagination_params)
|
||||
end
|
||||
|
||||
def readme_path
|
||||
|
|
|
@ -5,12 +5,20 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
|
|||
|
||||
presents ::Commit, as: :commit
|
||||
|
||||
def status_for(ref)
|
||||
def detailed_status_for(ref)
|
||||
return unless can?(current_user, :read_pipeline, commit.latest_pipeline(ref))
|
||||
return unless can?(current_user, :read_commit_status, commit.project)
|
||||
|
||||
commit.latest_pipeline(ref)&.detailed_status(current_user)
|
||||
end
|
||||
|
||||
def status_for(ref = nil)
|
||||
return unless can?(current_user, :read_pipeline, commit.latest_pipeline(ref))
|
||||
return unless can?(current_user, :read_commit_status, commit.project)
|
||||
|
||||
commit.status(ref)
|
||||
end
|
||||
|
||||
def any_pipelines?
|
||||
return false unless can?(current_user, :read_pipeline, commit.project)
|
||||
|
||||
|
|
30
app/validators/bytesize_validator.rb
Normal file
30
app/validators/bytesize_validator.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# BytesizeValidator
|
||||
#
|
||||
# Custom validator for verifying that bytesize of a field doesn't exceed the specified limit.
|
||||
# It is different from Rails length validator because it takes .bytesize into account instead of .size/.length
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class Snippet < ActiveRecord::Base
|
||||
# validates :content, bytesize: { maximum: -> { Gitlab::CurrentSettings.snippet_size_limit } }
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>maximum</tt> - Proc that evaluates the bytesize limit that cannot be exceeded
|
||||
class BytesizeValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attr, value)
|
||||
size = value.to_s.bytesize
|
||||
max_size = options[:maximum].call
|
||||
|
||||
return if size <= max_size
|
||||
|
||||
error_message = format(_('is too long (%{size}). The maximum size is %{max_size}.'), {
|
||||
size: ActiveSupport::NumberHelper.number_to_human_size(size),
|
||||
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size)
|
||||
})
|
||||
|
||||
record.errors.add(attr, error_message)
|
||||
end
|
||||
end
|
|
@ -14,7 +14,7 @@
|
|||
- project = local_assigns.fetch(:project) { merge_request&.project }
|
||||
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
|
||||
- commit = commit.present(current_user: current_user)
|
||||
- commit_status = commit.status_for(ref)
|
||||
- commit_status = commit.detailed_status_for(ref)
|
||||
- collapsible = local_assigns.fetch(:collapsible, true)
|
||||
- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
|
||||
- link = commit_path(project, commit, merge_request: merge_request)
|
||||
|
|
35
config/initializers/rack_VULNDB-255039_patch.rb
Normal file
35
config/initializers/rack_VULNDB-255039_patch.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if Gem.loaded_specs['rack'].version >= Gem::Version.new("3.0.0")
|
||||
raise <<~ERR
|
||||
This patch is unnecessary in Rack versions 3.0.0 or newer.
|
||||
Please remove this file and the associated spec.
|
||||
|
||||
See https://github.com/rack/rack/blob/main/CHANGELOG.md#security (issue #1733)
|
||||
ERR
|
||||
end
|
||||
|
||||
# Patches a cache poisoning attack vector in Rack by not allowing semicolons
|
||||
# to delimit query parameters.
|
||||
# See https://github.com/rack/rack/issues/1732.
|
||||
#
|
||||
# Solution is taken from the same issue.
|
||||
#
|
||||
# The actual patch is due for release in Rack 3.0.0.
|
||||
module Rack
|
||||
class Request
|
||||
Helpers.module_eval do
|
||||
# rubocop: disable Naming/MethodName
|
||||
def GET
|
||||
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
|
||||
get_header(RACK_REQUEST_QUERY_HASH)
|
||||
else
|
||||
query_hash = parse_query(query_string, '&') # only allow ampersand here
|
||||
set_header(RACK_REQUEST_QUERY_STRING, query_string)
|
||||
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
|
||||
end
|
||||
end
|
||||
# rubocop: enable Naming/MethodName
|
||||
end
|
||||
end
|
||||
end
|
44
config/initializers/sawyer_patch.rb
Normal file
44
config/initializers/sawyer_patch.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
# This patch updates SawyerResource class to not allow Ruby methods to be overridden and accessed.
|
||||
# Any attempt to access a Ruby method will result in an exception.
|
||||
module SawyerClassPatch
|
||||
def attr_accessor(*attrs)
|
||||
attrs.each do |attribute|
|
||||
class_eval do
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
if method_defined?(attribute) || method_defined?("#{attribute}=") || method_defined?("#{attribute}?")
|
||||
define_method attribute do
|
||||
raise Sawyer::Error,
|
||||
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
|
||||
end
|
||||
|
||||
define_method "#{attribute}=" do |value|
|
||||
raise Sawyer::Error,
|
||||
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
|
||||
end
|
||||
|
||||
define_method "#{attribute}?" do
|
||||
raise Sawyer::Error,
|
||||
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
|
||||
end
|
||||
else
|
||||
define_method attribute do
|
||||
@attrs[attribute.to_sym]
|
||||
end
|
||||
|
||||
define_method "#{attribute}=" do |value|
|
||||
@attrs[attribute.to_sym] = value
|
||||
end
|
||||
|
||||
define_method "#{attribute}?" do
|
||||
!!@attrs[attribute.to_sym]
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sawyer::Resource.singleton_class.prepend(SawyerClassPatch)
|
|
@ -267,3 +267,8 @@ To resolve this issue, you can update the password expiration by either:
|
|||
```
|
||||
|
||||
The bug was reported [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332455).
|
||||
|
||||
## Error on Git fetch: "HTTP Basic: Access Denied"
|
||||
|
||||
If you receive an `HTTP Basic: Access denied` error when using Git over HTTP(S),
|
||||
refer to the [two-factor authentication troubleshooting guide](../../user/profile/account/two_factor_authentication.md#troubleshooting).
|
||||
|
|
|
@ -299,6 +299,10 @@ hub_docker_quota_check:
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
## Authentication error: "HTTP Basic: Access Denied"
|
||||
|
||||
If you receive an `HTTP Basic: Access denied` error when authenticating against the Dependency Proxy, refer to the [two-factor authentication troubleshooting guide](../../profile/account/two_factor_authentication.md#troubleshooting).
|
||||
|
||||
### Dependency Proxy Connection Failure
|
||||
|
||||
If a service alias is not set the `docker:20.10.16` image is unable to find the
|
||||
|
|
|
@ -345,6 +345,11 @@ when a PyPI package is not found in the Package Registry, the request is forward
|
|||
|
||||
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
|
||||
|
||||
WARNING:
|
||||
When you use the `--index-url` option, do not specify the port if it is a default
|
||||
port, such as `80` for a URL starting with `http` or `443` for a URL starting
|
||||
with `https`.
|
||||
|
||||
### Install from the project level
|
||||
|
||||
To install the latest version of a package, use the following command:
|
||||
|
|
|
@ -427,6 +427,39 @@ a GitLab global administrator disable 2FA for your account:
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "HTTP Basic: Access denied. The provided password or token ..."
|
||||
|
||||
When making a request, you can receive the following error:
|
||||
|
||||
```plaintext
|
||||
HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal
|
||||
access token instead of a password.
|
||||
```
|
||||
|
||||
This error occurs in the following scenarios:
|
||||
|
||||
- You have 2FA enabled and have attempted to authenticate with a username and
|
||||
password. For 2FA-enabled users, a [personal access token](../personal_access_tokens.md) (PAT)
|
||||
must be used instead of a password. To authenticate:
|
||||
- Git requests over HTTP(S), a PAT with `read_repository` or `write_repository` scope is required.
|
||||
- [GitLab Container Registry](../../packages/container_registry/index.md#authenticate-with-the-container-registry) requests, a PAT
|
||||
with `read_registry` or `write_registry` scope is required.
|
||||
- [Dependency Proxy](../../packages/dependency_proxy/index.md#authenticate-with-the-dependency-proxy) requests, a PAT with
|
||||
`read_registry` and `write_registry` scopes is required.
|
||||
- You do not have 2FA enabled and have sent an incorrect username or password
|
||||
with your request.
|
||||
- You do not have 2FA enabled but an administrator has enabled the
|
||||
[enforce 2FA for all users](../../../security/two_factor_authentication.md#enforce-2fa-for-all-users) setting.
|
||||
- You do not have 2FA enabled, but an administrator has disabled the
|
||||
[password authentication enabled for Git over HTTP(S)](../../admin_area/settings/sign_in_restrictions.md#password-authentication-enabled)
|
||||
setting. If LDAP is:
|
||||
- Configured, an [LDAP password](../../../administration/auth/ldap/index.md)
|
||||
or a [personal access token](../personal_access_tokens.md)
|
||||
must be used to authenticate Git requests over HTTP(S).
|
||||
- Not configured, you must use a [personal access token](../personal_access_tokens.md).
|
||||
|
||||
### Error: "invalid pin code"
|
||||
|
||||
If you receive an `invalid pin code` error, this can indicate that there is a time sync issue between the authentication
|
||||
application and the GitLab instance itself. To avoid the time sync issue, enable time synchronization in the device that
|
||||
generates the codes. For example:
|
||||
|
|
|
@ -144,7 +144,7 @@ module API
|
|||
Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user, project: user_project)
|
||||
end
|
||||
|
||||
present commit_detail, with: Entities::CommitDetail, stats: params[:stats]
|
||||
present commit_detail, with: Entities::CommitDetail, include_stats: params[:stats], current_user: current_user
|
||||
else
|
||||
render_api_error!(result[:message], 400)
|
||||
end
|
||||
|
@ -163,7 +163,7 @@ module API
|
|||
|
||||
not_found! 'Commit' unless commit
|
||||
|
||||
present commit, with: Entities::CommitDetail, stats: params[:stats], current_user: current_user
|
||||
present commit, with: Entities::CommitDetail, include_stats: params[:stats], current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Get the diff for a specific commit of a project' do
|
||||
|
|
|
@ -12,7 +12,9 @@ module API
|
|||
expose :trailers
|
||||
|
||||
expose :web_url do |commit, _options|
|
||||
Gitlab::UrlBuilder.build(commit)
|
||||
c = commit
|
||||
c = c.__subject__ if c.is_a?(Gitlab::View::Presenter::Base)
|
||||
Gitlab::UrlBuilder.build(c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
module API
|
||||
module Entities
|
||||
class CommitDetail < Commit
|
||||
expose :stats, using: Entities::CommitStats, if: :stats
|
||||
expose :status
|
||||
include ::API::Helpers::Presentable
|
||||
|
||||
expose :stats, using: Entities::CommitStats, if: :include_stats
|
||||
expose :status_for, as: :status
|
||||
expose :project_id
|
||||
|
||||
expose :last_pipeline do |commit, options|
|
||||
|
|
|
@ -14,28 +14,12 @@ module API
|
|||
include Constants
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def unauthorized_user_project
|
||||
@unauthorized_user_project ||= find_project(params[:id])
|
||||
end
|
||||
|
||||
def unauthorized_user_project!
|
||||
unauthorized_user_project || not_found!
|
||||
end
|
||||
|
||||
def unauthorized_user_group
|
||||
@unauthorized_user_group ||= find_group(params[:id])
|
||||
end
|
||||
|
||||
def unauthorized_user_group!
|
||||
unauthorized_user_group || not_found!
|
||||
end
|
||||
|
||||
def authorized_user_project
|
||||
@authorized_user_project ||= authorized_project_find!
|
||||
end
|
||||
|
||||
def authorized_project_find!
|
||||
project = unauthorized_user_project
|
||||
project = find_project(params[:id])
|
||||
|
||||
unless project && can?(current_user, :read_project, project)
|
||||
return unauthorized_or! { not_found! }
|
||||
|
|
|
@ -84,6 +84,16 @@ module API
|
|||
|
||||
body content
|
||||
end
|
||||
|
||||
def ensure_group!
|
||||
find_group(params[:id]) || not_found!
|
||||
find_authorized_group!
|
||||
end
|
||||
|
||||
def ensure_project!
|
||||
find_project(params[:id]) || not_found!
|
||||
authorized_user_project
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
|
@ -91,7 +101,7 @@ module API
|
|||
end
|
||||
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
after_validation do
|
||||
unauthorized_user_group!
|
||||
ensure_group!
|
||||
end
|
||||
|
||||
namespace ':id/-/packages/pypi' do
|
||||
|
@ -101,7 +111,8 @@ module API
|
|||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'files/:sha256/*file_identifier' do
|
||||
group = unauthorized_user_group!
|
||||
group = find_authorized_group!
|
||||
authorize_read_package!(group)
|
||||
|
||||
filename = "#{params[:file_identifier]}.#{params[:format]}"
|
||||
package = Packages::Pypi::PackageFinder.new(current_user, group, { filename: filename, sha256: params[:sha256] }).execute
|
||||
|
@ -146,7 +157,7 @@ module API
|
|||
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
before do
|
||||
unauthorized_user_project!
|
||||
ensure_project!
|
||||
end
|
||||
|
||||
namespace ':id/packages/pypi' do
|
||||
|
@ -160,7 +171,8 @@ module API
|
|||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'files/:sha256/*file_identifier' do
|
||||
project = unauthorized_user_project!
|
||||
project = authorized_user_project
|
||||
authorize_read_package!(project)
|
||||
|
||||
filename = "#{params[:file_identifier]}.#{params[:format]}"
|
||||
package = Packages::Pypi::PackageFinder.new(current_user, project, { filename: filename, sha256: params[:sha256] }).execute
|
||||
|
|
|
@ -189,7 +189,7 @@ module API
|
|||
compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight])
|
||||
|
||||
if compare
|
||||
present compare, with: Entities::Compare
|
||||
present compare, with: Entities::Compare, current_user: current_user
|
||||
else
|
||||
not_found!("Ref")
|
||||
end
|
||||
|
|
|
@ -123,7 +123,7 @@ module API
|
|||
get do
|
||||
verify_search_scope!(resource: nil)
|
||||
|
||||
present search, with: entity
|
||||
present search, with: entity, current_user: current_user
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -145,7 +145,7 @@ module API
|
|||
get ':id/(-/)search' do
|
||||
verify_search_scope!(resource: user_group)
|
||||
|
||||
present search(group_id: user_group.id), with: entity
|
||||
present search(group_id: user_group.id), with: entity, current_user: current_user
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -166,7 +166,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
get ':id/(-/)search' do
|
||||
present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity
|
||||
present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity, current_user: current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ module API
|
|||
|
||||
if result[:status] == :success
|
||||
commit_detail = user_project.repository.commit(result[:result])
|
||||
present commit_detail, with: Entities::CommitDetail
|
||||
present commit_detail, with: Entities::CommitDetail, current_user: current_user
|
||||
else
|
||||
render_api_error!(result[:message], result[:http_status] || 400)
|
||||
end
|
||||
|
|
|
@ -17,21 +17,10 @@ module Banzai
|
|||
include ActionView::Helpers::TagHelper
|
||||
include AvatarsHelper
|
||||
|
||||
TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
|
||||
AUTHOR_REGEXP = /(?<author_name>.+)/.freeze
|
||||
# Devise.email_regexp wouldn't work here since its designed to match
|
||||
# against strings that only contains email addresses; the \A and \z
|
||||
# around the expression will only match if the string being matched
|
||||
# contains just the email nothing else.
|
||||
MAIL_REGEXP = /<(?<author_email>[^@\s]+@[^@\s]+)>/.freeze
|
||||
FILTER_REGEXP = /(?<trailer>^\s*#{TRAILER_REGEXP}\s*#{AUTHOR_REGEXP}\s+#{MAIL_REGEXP}$)/mi.freeze
|
||||
|
||||
def call
|
||||
doc.xpath('descendant-or-self::text()').each do |node|
|
||||
content = node.to_html
|
||||
|
||||
next unless content.match(FILTER_REGEXP)
|
||||
|
||||
html = trailer_filter(content)
|
||||
|
||||
next if html == content
|
||||
|
@ -52,11 +41,24 @@ module Banzai
|
|||
# Returns a String with all trailer lines replaced with links to GitLab
|
||||
# users and mailto links to non GitLab users. All links have `data-trailer`
|
||||
# and `data-user` attributes attached.
|
||||
#
|
||||
# The code intentionally avoids using Regex for security and performance
|
||||
# reasons: https://gitlab.com/gitlab-org/gitlab/-/issues/363734
|
||||
def trailer_filter(text)
|
||||
text.gsub(FILTER_REGEXP) do |author_match|
|
||||
label = $~[:label]
|
||||
"#{label} #{parse_user($~[:author_name], $~[:author_email], label)}"
|
||||
end
|
||||
text.lines.map! do |line|
|
||||
trailer, rest = line.split(':', 2)
|
||||
|
||||
next line unless trailer.downcase.end_with?('-by') && rest.present?
|
||||
|
||||
chunks = rest.split
|
||||
author_email = chunks.pop.delete_prefix('<').delete_suffix('>')
|
||||
next line unless Devise.email_regexp.match(author_email)
|
||||
|
||||
author_name = chunks.join(' ').strip
|
||||
trailer = "#{trailer.strip}:"
|
||||
|
||||
"#{trailer} #{link_to_user_or_email(author_name, author_email, trailer)}\n"
|
||||
end.join
|
||||
end
|
||||
|
||||
# Find a GitLab user using the supplied email and generate
|
||||
|
@ -67,7 +69,7 @@ module Banzai
|
|||
# trailer - String trailer used in the commit message
|
||||
#
|
||||
# Returns a String with a link to the user.
|
||||
def parse_user(name, email, trailer)
|
||||
def link_to_user_or_email(name, email, trailer)
|
||||
link_to_user User.find_by_any_email(email),
|
||||
name: name,
|
||||
email: email,
|
||||
|
|
|
@ -34,17 +34,20 @@ module Banzai
|
|||
img.remove_attribute('data-diagram-src')
|
||||
end
|
||||
|
||||
link.children = if link_replaces_image
|
||||
img['alt'] || img['data-src'] || img['src']
|
||||
else
|
||||
img.clone
|
||||
end
|
||||
link.children = link_replaces_image ? link_children(img) : img.clone
|
||||
|
||||
img.replace(link)
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_children(img)
|
||||
[img['alt'], img['data-src'], img['src']]
|
||||
.map { |f| Sanitize.fragment(f).presence }.compact.first || ''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
27
lib/banzai/filter/pathological_markdown_filter.rb
Normal file
27
lib/banzai/filter/pathological_markdown_filter.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module Filter
|
||||
class PathologicalMarkdownFilter < HTML::Pipeline::TextFilter
|
||||
# It's not necessary for this to be precise - we just need to detect
|
||||
# when there are a non-trivial number of unclosed image links.
|
||||
# So we don't really care about code blocks, etc.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/370428
|
||||
REGEX = /!\[(?:[^\]])+?!\[/.freeze
|
||||
DETECTION_MAX = 10
|
||||
|
||||
def call
|
||||
count = 0
|
||||
|
||||
@text.scan(REGEX) do |_match|
|
||||
count += 1
|
||||
break if count > DETECTION_MAX
|
||||
end
|
||||
|
||||
return @text if count <= DETECTION_MAX
|
||||
|
||||
"_Unable to render markdown - too many unclosed markdown image links detected._"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ module Banzai
|
|||
class PlainMarkdownPipeline < BasePipeline
|
||||
def self.filters
|
||||
FilterArray[
|
||||
Filter::PathologicalMarkdownFilter,
|
||||
Filter::MarkdownPreEscapeFilter,
|
||||
Filter::MarkdownFilter,
|
||||
Filter::MarkdownPostEscapeFilter
|
||||
|
|
|
@ -16,9 +16,10 @@ module Gitlab
|
|||
TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze
|
||||
|
||||
override :tree_entries
|
||||
def tree_entries(repository, sha, path, recursive, pagination_params = nil)
|
||||
def tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params = nil)
|
||||
if use_rugged?(repository, :rugged_tree_entries)
|
||||
entries = execute_rugged_call(:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive)
|
||||
entries = execute_rugged_call(
|
||||
:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive, skip_flat_paths)
|
||||
|
||||
if pagination_params
|
||||
paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
|
||||
|
@ -60,11 +61,11 @@ module Gitlab
|
|||
[result, cursor]
|
||||
end
|
||||
|
||||
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive)
|
||||
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive, skip_flat_paths)
|
||||
tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
|
||||
# This was an optimization to reduce N+1 queries for Gitaly
|
||||
# (https://gitlab.com/gitlab-org/gitaly/issues/530).
|
||||
rugged_populate_flat_path(repository, sha, path, entries)
|
||||
rugged_populate_flat_path(repository, sha, path, entries) unless skip_flat_paths
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,15 +15,16 @@ module Gitlab
|
|||
# Uses rugged for raw objects
|
||||
#
|
||||
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
|
||||
def where(repository, sha, path = nil, recursive = false, pagination_params = nil)
|
||||
def where(repository, sha, path = nil, recursive = false, skip_flat_paths = true, pagination_params = nil)
|
||||
path = nil if path == '' || path == '/'
|
||||
|
||||
tree_entries(repository, sha, path, recursive, pagination_params)
|
||||
tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params)
|
||||
end
|
||||
|
||||
def tree_entries(repository, sha, path, recursive, pagination_params = nil)
|
||||
def tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params = nil)
|
||||
wrapped_gitaly_errors do
|
||||
repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive, pagination_params)
|
||||
repository.gitaly_commit_client.tree_entries(
|
||||
repository, sha, path, recursive, skip_flat_paths, pagination_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ module Gitlab
|
|||
class CommitService
|
||||
include Gitlab::EncodingHelper
|
||||
|
||||
TREE_ENTRIES_DEFAULT_LIMIT = 100_000
|
||||
|
||||
def initialize(repository)
|
||||
@gitaly_repo = repository.gitaly_repository
|
||||
@repository = repository
|
||||
|
@ -111,12 +113,16 @@ module Gitlab
|
|||
nil
|
||||
end
|
||||
|
||||
def tree_entries(repository, revision, path, recursive, pagination_params)
|
||||
def tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params)
|
||||
pagination_params ||= {}
|
||||
pagination_params[:limit] ||= TREE_ENTRIES_DEFAULT_LIMIT
|
||||
|
||||
request = Gitaly::GetTreeEntriesRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
revision: encode_binary(revision),
|
||||
path: path.present? ? encode_binary(path) : '.',
|
||||
recursive: recursive,
|
||||
skip_flat_paths: skip_flat_paths,
|
||||
pagination_params: pagination_params
|
||||
)
|
||||
request.sort = Gitaly::GetTreeEntriesRequest::SortBy::TREES_FIRST if pagination_params
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
# this if the change to the renderer output is a new feature or a
|
||||
# minor bug fix.
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330313
|
||||
CACHE_COMMONMARK_VERSION = 31
|
||||
CACHE_COMMONMARK_VERSION = 32
|
||||
CACHE_COMMONMARK_VERSION_START = 10
|
||||
|
||||
BaseError = Class.new(StandardError)
|
||||
|
|
|
@ -68,6 +68,10 @@ module Gitlab
|
|||
with { |redis| redis.ttl(cache_key(key)) }
|
||||
end
|
||||
|
||||
def count(key)
|
||||
with { |redis| redis.scard(cache_key(key)) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with(&blk)
|
||||
|
|
|
@ -5,6 +5,10 @@ module Gitlab
|
|||
class Client
|
||||
Error = Class.new(StandardError)
|
||||
ConfigError = Class.new(Error)
|
||||
RequestError = Class.new(Error)
|
||||
|
||||
CACHE_MAX_SET_SIZE = 5_000
|
||||
CACHE_TTL = 1.month.freeze
|
||||
|
||||
attr_reader :integration
|
||||
|
||||
|
@ -33,11 +37,21 @@ module Gitlab
|
|||
end
|
||||
|
||||
def fetch_issues(params = {})
|
||||
get("products/#{zentao_product_xid}/issues", params)
|
||||
get("products/#{zentao_product_xid}/issues", params).tap do |response|
|
||||
mark_issues_as_seen_in_product(response['issues'])
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_issue(issue_id)
|
||||
raise Gitlab::Zentao::Client::Error, 'invalid issue id' unless issue_id_pattern.match(issue_id)
|
||||
raise Error, 'invalid issue id' unless issue_id_pattern.match(issue_id)
|
||||
|
||||
# Only return issues that are associated with the product configured in
|
||||
# the integration. Due to a lack of available data in the ZenTao APIs, we
|
||||
# can only determine if an issue belongs to a product if the issue was
|
||||
# previously returned in the `#fetch_issues` call.
|
||||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/360372#note_1016963713
|
||||
raise RequestError unless issue_seen_in_product?(issue_id)
|
||||
|
||||
get("issues/#{issue_id}")
|
||||
end
|
||||
|
@ -52,17 +66,15 @@ module Gitlab
|
|||
options = { headers: headers, query: params }
|
||||
response = Gitlab::HTTP.get(url(path), options)
|
||||
|
||||
raise Gitlab::Zentao::Client::Error, 'request error' unless response.success?
|
||||
raise RequestError unless response.success?
|
||||
|
||||
Gitlab::Json.parse(response.body)
|
||||
rescue JSON::ParserError
|
||||
raise Gitlab::Zentao::Client::Error, 'invalid response format'
|
||||
raise Error, 'invalid response format'
|
||||
end
|
||||
|
||||
def url(path)
|
||||
host = integration.api_url.presence || integration.url
|
||||
|
||||
URI.parse(Gitlab::Utils.append_path(host, "api.php/v1/#{path}"))
|
||||
URI.parse(Gitlab::Utils.append_path(integration.client_url, "api.php/v1/#{path}"))
|
||||
end
|
||||
|
||||
def headers
|
||||
|
@ -75,6 +87,30 @@ module Gitlab
|
|||
def zentao_product_xid
|
||||
integration.zentao_product_xid
|
||||
end
|
||||
|
||||
def issue_ids_cache_key
|
||||
@issue_ids_cache_key ||= [
|
||||
:zentao_product_issues,
|
||||
OpenSSL::Digest::SHA256.hexdigest(integration.client_url),
|
||||
zentao_product_xid
|
||||
].join(':')
|
||||
end
|
||||
|
||||
def issue_ids_cache
|
||||
@issue_ids_cache ||= ::Gitlab::SetCache.new(expires_in: CACHE_TTL)
|
||||
end
|
||||
|
||||
def mark_issues_as_seen_in_product(issues)
|
||||
return unless issues && issue_ids_cache.count(issue_ids_cache_key) < CACHE_MAX_SET_SIZE
|
||||
|
||||
ids = issues.map { _1['id'] }
|
||||
|
||||
issue_ids_cache.write(issue_ids_cache_key, ids)
|
||||
end
|
||||
|
||||
def issue_seen_in_product?(id)
|
||||
issue_ids_cache.include?(issue_ids_cache_key, id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19065,7 +19065,7 @@ msgstr ""
|
|||
msgid "HTTP Archive (HAR)"
|
||||
msgstr ""
|
||||
|
||||
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
|
||||
msgid "HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Harbor Registry"
|
||||
|
@ -46573,6 +46573,9 @@ msgstr ""
|
|||
msgid "is too long (%{current_value}). The maximum size is %{max_size}."
|
||||
msgstr ""
|
||||
|
||||
msgid "is too long (%{size}). The maximum size is %{max_size}."
|
||||
msgstr ""
|
||||
|
||||
msgid "is too long (maximum is %{count} characters)"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"@apollo/client": "^3.5.10",
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@codesandbox/sandpack-client": "^1.2.2",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "3.1.0",
|
||||
|
@ -164,7 +165,6 @@
|
|||
"remark-rehype": "^10.1.0",
|
||||
"scrollparent": "^2.0.1",
|
||||
"select2": "3.5.2-browserify",
|
||||
"smooshpack": "^0.0.62",
|
||||
"sortablejs": "^1.10.2",
|
||||
"string-hash": "1.1.3",
|
||||
"style-loader": "^2.0.0",
|
||||
|
|
|
@ -30,9 +30,16 @@ module QA
|
|||
end
|
||||
|
||||
let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
|
||||
let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
|
||||
let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
|
||||
let(:personal_access_token) { use_ci_variable(name: 'PERSONAL_ACCESS_TOKEN', value: Runtime::Env.personal_access_token, project: project) }
|
||||
let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
|
||||
let(:gitlab_host_with_port) do
|
||||
# Don't specify port if it is a standard one
|
||||
if uri.port == 80 || uri.port == 443
|
||||
uri.host
|
||||
else
|
||||
"#{uri.host}:#{uri.port}"
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
|
|
|
@ -2,15 +2,15 @@ import { GlLoadingIcon } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { dispatch } from 'codesandbox-api';
|
||||
import smooshpack from 'smooshpack';
|
||||
import { SandpackClient } from '@codesandbox/sandpack-client';
|
||||
import Vuex from 'vuex';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import Clientside from '~/ide/components/preview/clientside.vue';
|
||||
import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
|
||||
import eventHub from '~/ide/eventhub';
|
||||
|
||||
jest.mock('smooshpack', () => ({
|
||||
Manager: jest.fn(),
|
||||
jest.mock('@codesandbox/sandpack-client', () => ({
|
||||
SandpackClient: jest.fn(),
|
||||
}));
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
@ -78,8 +78,8 @@ describe('IDE clientside preview', () => {
|
|||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
sandpackReady: true,
|
||||
manager: {
|
||||
listener: jest.fn(),
|
||||
client: {
|
||||
cleanup: jest.fn(),
|
||||
updatePreview: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
@ -90,9 +90,9 @@ describe('IDE clientside preview', () => {
|
|||
});
|
||||
|
||||
describe('without main entry', () => {
|
||||
it('creates sandpack manager', () => {
|
||||
it('creates sandpack client', () => {
|
||||
createComponent();
|
||||
expect(smooshpack.Manager).not.toHaveBeenCalled();
|
||||
expect(SandpackClient).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('with main entry', () => {
|
||||
|
@ -102,8 +102,8 @@ describe('IDE clientside preview', () => {
|
|||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('creates sandpack manager', () => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalledWith(
|
||||
it('creates sandpack client', () => {
|
||||
expect(SandpackClient).toHaveBeenCalledWith(
|
||||
'#ide-preview',
|
||||
expectedSandpackOptions(),
|
||||
expectedSandpackSettings(),
|
||||
|
@ -141,8 +141,8 @@ describe('IDE clientside preview', () => {
|
|||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('creates sandpack manager with bundlerURL', () => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
|
||||
it('creates sandpack client with bundlerURL', () => {
|
||||
expect(SandpackClient).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
|
||||
...expectedSandpackSettings(),
|
||||
bundlerURL: TEST_BUNDLER_URL,
|
||||
});
|
||||
|
@ -156,8 +156,8 @@ describe('IDE clientside preview', () => {
|
|||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('creates sandpack manager', () => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalledWith(
|
||||
it('creates sandpack client', () => {
|
||||
expect(SandpackClient).toHaveBeenCalledWith(
|
||||
'#ide-preview',
|
||||
{
|
||||
files: {},
|
||||
|
@ -332,7 +332,7 @@ describe('IDE clientside preview', () => {
|
|||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('initializes manager if manager is empty', () => {
|
||||
it('initializes client if client is empty', () => {
|
||||
createComponent({ getters: { packageJson: dummyPackageJson } });
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
@ -340,7 +340,7 @@ describe('IDE clientside preview', () => {
|
|||
wrapper.vm.update();
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalled();
|
||||
expect(SandpackClient).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -349,7 +349,7 @@ describe('IDE clientside preview', () => {
|
|||
|
||||
wrapper.vm.update();
|
||||
|
||||
expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -361,7 +361,7 @@ describe('IDE clientside preview', () => {
|
|||
});
|
||||
|
||||
it('calls updatePreview', () => {
|
||||
expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -405,7 +405,7 @@ describe('IDE clientside preview', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
createInitializedComponent();
|
||||
spy = wrapper.vm.manager.updatePreview;
|
||||
spy = wrapper.vm.client.updatePreview;
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ jest.mock('codesandbox-api', () => ({
|
|||
|
||||
describe('IDE clientside preview navigator', () => {
|
||||
let wrapper;
|
||||
let manager;
|
||||
let client;
|
||||
let listenHandler;
|
||||
|
||||
const findBackButton = () => wrapper.findAll('button').at(0);
|
||||
|
@ -20,9 +20,9 @@ describe('IDE clientside preview navigator', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
listen.mockClear();
|
||||
manager = { bundlerURL: TEST_HOST, iframe: { src: '' } };
|
||||
client = { bundlerURL: TEST_HOST, iframe: { src: '' } };
|
||||
|
||||
wrapper = shallowMount(ClientsideNavigator, { propsData: { manager } });
|
||||
wrapper = shallowMount(ClientsideNavigator, { propsData: { client } });
|
||||
[[listenHandler]] = listen.mock.calls;
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,7 @@ describe('IDE clientside preview navigator', () => {
|
|||
});
|
||||
|
||||
it('renders readonly URL bar', async () => {
|
||||
listenHandler({ type: 'urlchange', url: manager.bundlerURL });
|
||||
listenHandler({ type: 'urlchange', url: client.bundlerURL });
|
||||
await nextTick();
|
||||
expect(wrapper.find('input[readonly]').element.value).toBe('/');
|
||||
});
|
||||
|
@ -89,13 +89,13 @@ describe('IDE clientside preview navigator', () => {
|
|||
expect(findBackButton().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('updates manager iframe src', async () => {
|
||||
it('updates client iframe src', async () => {
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
|
||||
await nextTick();
|
||||
findBackButton().trigger('click');
|
||||
|
||||
expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -133,13 +133,13 @@ describe('IDE clientside preview navigator', () => {
|
|||
expect(findForwardButton().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('updates manager iframe src', async () => {
|
||||
it('updates client iframe src', async () => {
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
|
||||
await nextTick();
|
||||
findBackButton().trigger('click');
|
||||
|
||||
expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -152,10 +152,10 @@ describe('IDE clientside preview navigator', () => {
|
|||
});
|
||||
|
||||
it('calls refresh with current path', () => {
|
||||
manager.iframe.src = 'something-other';
|
||||
client.iframe.src = 'something-other';
|
||||
findRefreshButton().trigger('click');
|
||||
|
||||
expect(manager.iframe.src).toBe(url);
|
||||
expect(client.iframe.src).toBe(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ export default [
|
|||
'</tr>\n',
|
||||
'</table>',
|
||||
].join(''),
|
||||
output: '<table>',
|
||||
output: '<table data-myattr="XSS">',
|
||||
},
|
||||
],
|
||||
// Note: style is sanitized out
|
||||
|
@ -98,7 +98,7 @@ export default [
|
|||
'</svg>',
|
||||
].join(),
|
||||
output:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="388.84pt" version="1.0" id="svg2" height="115.02pt">',
|
||||
'<svg height="115.02pt" id="svg2" version="1.0" width="388.84pt" xmlns="http://www.w3.org/2000/svg">',
|
||||
},
|
||||
],
|
||||
];
|
||||
|
|
|
@ -49,15 +49,17 @@ describe('Output component', () => {
|
|||
const htmlType = json.cells[4];
|
||||
createComponent(htmlType.outputs[0]);
|
||||
|
||||
expect(wrapper.findAll('p')).toHaveLength(1);
|
||||
expect(wrapper.text()).toContain('test');
|
||||
const iframe = wrapper.find('iframe');
|
||||
expect(iframe.exists()).toBe(true);
|
||||
expect(iframe.element.getAttribute('sandbox')).toBe('');
|
||||
expect(iframe.element.getAttribute('srcdoc')).toBe('<p>test</p>');
|
||||
});
|
||||
|
||||
it('renders multiple raw HTML outputs', () => {
|
||||
const htmlType = json.cells[4];
|
||||
createComponent([htmlType.outputs[0], htmlType.outputs[0]]);
|
||||
|
||||
expect(wrapper.findAll('p')).toHaveLength(2);
|
||||
expect(wrapper.findAll('iframe')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,7 +86,11 @@ describe('Output component', () => {
|
|||
});
|
||||
|
||||
it('renders as an svg', () => {
|
||||
expect(wrapper.find('svg').exists()).toBe(true);
|
||||
const iframe = wrapper.find('iframe');
|
||||
|
||||
expect(iframe.exists()).toBe(true);
|
||||
expect(iframe.element.getAttribute('sandbox')).toBe('');
|
||||
expect(iframe.element.getAttribute('srcdoc')).toBe('<svg></svg>');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -320,7 +320,7 @@ RSpec.describe CommitsHelper do
|
|||
let(:current_path) { "test" }
|
||||
|
||||
before do
|
||||
expect(commit).to receive(:status_for).with(ref).and_return(commit_status)
|
||||
expect(commit).to receive(:detailed_status_for).with(ref).and_return(commit_status)
|
||||
assign(:path, current_path)
|
||||
end
|
||||
|
||||
|
|
|
@ -112,6 +112,14 @@ RSpec.describe LabelsHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'render_label_text' do
|
||||
it 'html escapes the bg_color correctly' do
|
||||
xss_payload = '"><img src=x onerror=prompt(1)>'
|
||||
label_text = render_label_text('xss', bg_color: xss_payload)
|
||||
expect(label_text).to include(html_escape(xss_payload))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'text_color_for_bg' do
|
||||
it 'uses light text on dark backgrounds' do
|
||||
expect(text_color_for_bg('#222E2E')).to be_color('#FFFFFF')
|
||||
|
|
17
spec/initializers/rack_VULNDB-255039_patch_spec.rb
Normal file
17
spec/initializers/rack_VULNDB-255039_patch_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Rack VULNDB-255039' do
|
||||
context 'when handling query params in GET requests' do
|
||||
it 'does not treat semicolons as query delimiters' do
|
||||
env = ::Rack::MockRequest.env_for('http://gitlab.com?a=b;c=1')
|
||||
|
||||
query_hash = ::Rack::Request.new(env).GET
|
||||
|
||||
# Prior to this patch, this was splitting around the semicolon, which
|
||||
# would return {"a"=>"b", "c"=>"1"}
|
||||
expect(query_hash).to eq({ "a" => "b;c=1" })
|
||||
end
|
||||
end
|
||||
end
|
69
spec/initializers/sawyer_patch_spec.rb
Normal file
69
spec/initializers/sawyer_patch_spec.rb
Normal file
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
require 'sawyer'
|
||||
|
||||
require_relative '../../config/initializers/sawyer_patch'
|
||||
|
||||
RSpec.describe 'sawyer_patch' do
|
||||
it 'raises error when acessing a method that overlaps a Ruby method' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
to_s: 'Overriding method',
|
||||
user: { to_s: 'Overriding method', name: 'User name' }
|
||||
}
|
||||
)
|
||||
|
||||
error_message = 'Sawyer method "to_s" overlaps Ruby method. Convert to a hash to access the attribute.'
|
||||
expect { sawyer_resource.to_s }.to raise_error(Sawyer::Error, error_message)
|
||||
expect { sawyer_resource.to_s? }.to raise_error(Sawyer::Error, error_message)
|
||||
expect { sawyer_resource.to_s = 'new value' }.to raise_error(Sawyer::Error, error_message)
|
||||
expect { sawyer_resource.user.to_s }.to raise_error(Sawyer::Error, error_message)
|
||||
expect(sawyer_resource.user.name).to eq('User name')
|
||||
end
|
||||
|
||||
it 'raises error when acessing a boolean method that overlaps a Ruby method' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
nil?: 'value'
|
||||
}
|
||||
)
|
||||
|
||||
expect { sawyer_resource.nil? }.to raise_error(Sawyer::Error)
|
||||
end
|
||||
|
||||
it 'raises error when acessing a method that expects an argument' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
'user': 'value',
|
||||
'user=': 'value',
|
||||
'==': 'value',
|
||||
'!=': 'value',
|
||||
'+': 'value'
|
||||
}
|
||||
)
|
||||
|
||||
expect(sawyer_resource.user).to eq('value')
|
||||
expect { sawyer_resource.user = 'New user' }.to raise_error(ArgumentError)
|
||||
expect { sawyer_resource == true }.to raise_error(ArgumentError)
|
||||
expect { sawyer_resource != true }.to raise_error(ArgumentError)
|
||||
expect { sawyer_resource + 1 }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'does not raise error if is not an overlapping method' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
count_total: 1,
|
||||
user: { name: 'User name' }
|
||||
}
|
||||
)
|
||||
|
||||
expect(sawyer_resource.count_total).to eq(1)
|
||||
expect(sawyer_resource.count_total?).to eq(true)
|
||||
expect(sawyer_resource.count_total + 1).to eq(2)
|
||||
expect(sawyer_resource.user.name).to eq('User name')
|
||||
end
|
||||
end
|
|
@ -18,10 +18,20 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
|
|||
context 'detects' do
|
||||
let(:email) { FFaker::Internet.email }
|
||||
|
||||
it 'trailers in the form of *-by and replace users with links' do
|
||||
doc = filter(commit_message_html)
|
||||
context 'trailers in the form of *-by' do
|
||||
where(:commit_trailer) do
|
||||
["#{FFaker::Lorem.word}-by:", "#{FFaker::Lorem.word}-BY:", "#{FFaker::Lorem.word}-By:"]
|
||||
end
|
||||
|
||||
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
|
||||
with_them do
|
||||
let(:trailer) { commit_trailer }
|
||||
|
||||
it 'replaces users with links' do
|
||||
doc = filter(commit_message_html)
|
||||
|
||||
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'trailers prefixed with whitespaces' do
|
||||
|
@ -121,7 +131,14 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
|
|||
|
||||
context "ignores" do
|
||||
it 'commit messages without trailers' do
|
||||
exp = message = commit_html(FFaker::Lorem.sentence)
|
||||
exp = message = commit_html(Array.new(5) { FFaker::Lorem.sentence }.join("\n"))
|
||||
doc = filter(message)
|
||||
|
||||
expect(doc.to_html).to match Regexp.escape(exp)
|
||||
end
|
||||
|
||||
it 'trailers without emails' do
|
||||
exp = message = commit_html(Array.new(5) { 'Merged-By:' }.join("\n"))
|
||||
doc = filter(message)
|
||||
|
||||
expect(doc.to_html).to match Regexp.escape(exp)
|
||||
|
|
|
@ -92,5 +92,50 @@ RSpec.describe Banzai::Filter::ImageLinkFilter do
|
|||
|
||||
expect(doc.at_css('a')['class']).to match(%r{with-attachment-icon})
|
||||
end
|
||||
|
||||
context 'when link attributes contain malicious code' do
|
||||
let(:malicious_code) do
|
||||
# rubocop:disable Layout/LineLength
|
||||
%q(<a class='fixed-top fixed-bottom' data-create-path=/malicious-url><style> .tab-content>.tab-pane{display: block !important}</style>)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
context 'when image alt contains malicious code' do
|
||||
it 'ignores image alt and uses image path as the link text', :aggregate_failures do
|
||||
doc = filter(image(path, alt: malicious_code), context)
|
||||
|
||||
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
|
||||
expect(doc.at_css('a')['href']).to eq(path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when image src contains malicious code' do
|
||||
it 'ignores image src and does not use it as the link text' do
|
||||
doc = filter(image(malicious_code), context)
|
||||
|
||||
expect(doc.to_html).to match(%r{^<a[^>]*></a>$})
|
||||
end
|
||||
|
||||
it 'keeps image src unchanged, malicious code does not execute as part of url' do
|
||||
doc = filter(image(malicious_code), context)
|
||||
|
||||
expect(doc.at_css('a')['href']).to eq(malicious_code)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when image data-src contains malicious code' do
|
||||
it 'ignores data-src and uses image path as the link text', :aggregate_failures do
|
||||
doc = filter(image(path, data_src: malicious_code), context)
|
||||
|
||||
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
|
||||
end
|
||||
|
||||
it 'uses image data-src, malicious code does not execute as part of url' do
|
||||
doc = filter(image(path, data_src: malicious_code), context)
|
||||
|
||||
expect(doc.at_css('a')['href']).to eq(malicious_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
27
spec/lib/banzai/filter/pathological_markdown_filter_spec.rb
Normal file
27
spec/lib/banzai/filter/pathological_markdown_filter_spec.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Banzai::Filter::PathologicalMarkdownFilter do
|
||||
include FilterSpecHelper
|
||||
|
||||
let_it_be(:short_text) { '![a' * 5 }
|
||||
let_it_be(:long_text) { ([short_text] * 10).join(' ') }
|
||||
let_it_be(:with_images_text) { "![One ![one](one.jpg) #{'and\n' * 200} ![two ![two](two.jpg)" }
|
||||
|
||||
it 'detects a significat number of unclosed image links' do
|
||||
msg = <<~TEXT
|
||||
_Unable to render markdown - too many unclosed markdown image links detected._
|
||||
TEXT
|
||||
|
||||
expect(filter(long_text)).to eq(msg.strip)
|
||||
end
|
||||
|
||||
it 'does nothing when there are only a few unclosed image links' do
|
||||
expect(filter(short_text)).to eq(short_text)
|
||||
end
|
||||
|
||||
it 'does nothing when there are only a few unclosed image links and images' do
|
||||
expect(filter(with_images_text)).to eq(with_images_text)
|
||||
end
|
||||
end
|
|
@ -167,4 +167,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
|
|||
expect(output).to include('<em>@test_</em>')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unclosed image links' do
|
||||
it 'detects a significat number of unclosed image links' do
|
||||
markdown = '![a ' * 30
|
||||
msg = <<~TEXT
|
||||
Unable to render markdown - too many unclosed markdown image links detected.
|
||||
TEXT
|
||||
output = described_class.to_html(markdown, project: nil)
|
||||
|
||||
expect(output).to include(msg.strip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,12 +9,13 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:repository) { project.repository.raw }
|
||||
|
||||
shared_examples :repo do
|
||||
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, pagination_params) }
|
||||
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, pagination_params) }
|
||||
|
||||
let(:sha) { SeedRepo::Commit::ID }
|
||||
let(:path) { nil }
|
||||
let(:recursive) { false }
|
||||
let(:pagination_params) { nil }
|
||||
let(:skip_flat_paths) { false }
|
||||
|
||||
let(:entries) { tree.first }
|
||||
let(:cursor) { tree.second }
|
||||
|
@ -107,6 +108,12 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
end
|
||||
|
||||
it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') }
|
||||
|
||||
context 'when skip_flat_paths is true' do
|
||||
let(:skip_flat_paths) { true }
|
||||
|
||||
it { expect(subdir_file.flat_path).to be_blank }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -162,7 +169,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
allow(instance).to receive(:lookup).with(SeedRepo::Commit::ID)
|
||||
end
|
||||
|
||||
described_class.where(repository, SeedRepo::Commit::ID, 'files', false)
|
||||
described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
|
||||
end
|
||||
|
||||
it_behaves_like :repo do
|
||||
|
@ -180,7 +187,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:entries_count) { entries.count }
|
||||
|
||||
it 'returns all entries without a cursor' do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: entries_count, page_token: nil })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: entries_count, page_token: nil })
|
||||
|
||||
expect(cursor).to be_nil
|
||||
expect(result.entries.count).to eq(entries_count)
|
||||
|
@ -209,7 +216,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:entries_count) { entries.count }
|
||||
|
||||
it 'returns all entries' do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: -1, page_token: nil })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: nil })
|
||||
|
||||
expect(result.count).to eq(entries_count)
|
||||
expect(cursor).to be_nil
|
||||
|
@ -220,7 +227,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:token) { entries.second.id }
|
||||
|
||||
it 'returns all entries after token' do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: -1, page_token: token })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: token })
|
||||
|
||||
expect(result.count).to eq(entries.count - 2)
|
||||
expect(cursor).to be_nil
|
||||
|
@ -252,7 +259,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
expected_entries = entries
|
||||
|
||||
loop do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: 5, page_token: token })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: 5, page_token: token })
|
||||
|
||||
collected_entries += result.entries
|
||||
token = cursor&.next_cursor
|
||||
|
|
|
@ -150,16 +150,18 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
|
|||
end
|
||||
|
||||
describe '#tree_entries' do
|
||||
subject { client.tree_entries(repository, revision, path, recursive, pagination_params) }
|
||||
subject { client.tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params) }
|
||||
|
||||
let(:path) { '/' }
|
||||
let(:recursive) { false }
|
||||
let(:pagination_params) { nil }
|
||||
let(:skip_flat_paths) { false }
|
||||
|
||||
it 'sends a get_tree_entries message' do
|
||||
it 'sends a get_tree_entries message with default limit' do
|
||||
expected_pagination_params = Gitaly::PaginationParameter.new(limit: Gitlab::GitalyClient::CommitService::TREE_ENTRIES_DEFAULT_LIMIT)
|
||||
expect_any_instance_of(Gitaly::CommitService::Stub)
|
||||
.to receive(:get_tree_entries)
|
||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
||||
.with(gitaly_request_with_params({ pagination_params: expected_pagination_params }), kind_of(Hash))
|
||||
.and_return([])
|
||||
|
||||
is_expected.to eq([[], nil])
|
||||
|
@ -189,9 +191,10 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
|
|||
pagination_cursor: pagination_cursor
|
||||
)
|
||||
|
||||
expected_pagination_params = Gitaly::PaginationParameter.new(limit: 3)
|
||||
expect_any_instance_of(Gitaly::CommitService::Stub)
|
||||
.to receive(:get_tree_entries)
|
||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
||||
.with(gitaly_request_with_params({ pagination_params: expected_pagination_params }), kind_of(Hash))
|
||||
.and_return([response])
|
||||
|
||||
is_expected.to eq([[], pagination_cursor])
|
||||
|
|
|
@ -72,4 +72,18 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
|
|||
it { is_expected.to be(true) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'count' do
|
||||
subject { cache.count(cache_prefix) }
|
||||
|
||||
it { is_expected.to be(0) }
|
||||
|
||||
context 'item added' do
|
||||
before do
|
||||
cache.write(cache_prefix, 'test_item')
|
||||
end
|
||||
|
||||
it { is_expected.to be(1) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Zentao::Client do
|
||||
subject(:integration) { described_class.new(zentao_integration) }
|
||||
RSpec.describe Gitlab::Zentao::Client, :clean_gitlab_redis_cache do
|
||||
subject(:client) { described_class.new(zentao_integration) }
|
||||
|
||||
let(:zentao_integration) { create(:zentao_integration) }
|
||||
|
||||
def mock_get_products_url
|
||||
integration.send(:url, "products/#{zentao_integration.zentao_product_xid}")
|
||||
client.send(:url, "products/#{zentao_integration.zentao_product_xid}")
|
||||
end
|
||||
|
||||
def mock_fetch_issues_url
|
||||
client.send(:url, "products/#{zentao_integration.zentao_product_xid}/issues")
|
||||
end
|
||||
|
||||
def mock_fetch_issue_url(issue_id)
|
||||
integration.send(:url, "issues/#{issue_id}")
|
||||
client.send(:url, "issues/#{issue_id}")
|
||||
end
|
||||
|
||||
let(:mock_headers) do
|
||||
|
@ -29,13 +33,13 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
let(:zentao_integration) { nil }
|
||||
|
||||
it 'raises ConfigError' do
|
||||
expect { integration }.to raise_error(described_class::ConfigError)
|
||||
expect { client }.to raise_error(described_class::ConfigError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'integration is provided' do
|
||||
it 'is initialized successfully' do
|
||||
expect { integration }.not_to raise_error
|
||||
expect { client }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -50,7 +54,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
end
|
||||
|
||||
it 'fetches the product' do
|
||||
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
|
||||
expect(client.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -62,8 +66,8 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
|
||||
it 'fetches the empty product' do
|
||||
expect do
|
||||
integration.fetch_product(zentao_integration.zentao_product_xid)
|
||||
end.to raise_error(Gitlab::Zentao::Client::Error, 'request error')
|
||||
client.fetch_product(zentao_integration.zentao_product_xid)
|
||||
end.to raise_error(Gitlab::Zentao::Client::RequestError)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -75,7 +79,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
|
||||
it 'fetches the empty product' do
|
||||
expect do
|
||||
integration.fetch_product(zentao_integration.zentao_product_xid)
|
||||
client.fetch_product(zentao_integration.zentao_product_xid)
|
||||
end.to raise_error(Gitlab::Zentao::Client::Error, 'invalid response format')
|
||||
end
|
||||
end
|
||||
|
@ -89,7 +93,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
end
|
||||
|
||||
it 'responds with success' do
|
||||
expect(integration.ping[:success]).to eq true
|
||||
expect(client.ping[:success]).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -100,7 +104,69 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
end
|
||||
|
||||
it 'responds with unsuccess' do
|
||||
expect(integration.ping[:success]).to eq false
|
||||
expect(client.ping[:success]).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_issues' do
|
||||
let(:mock_response) { { 'issues' => [{ 'id' => 'story-1' }, { 'id' => 'bug-11' }] } }
|
||||
|
||||
before do
|
||||
WebMock.stub_request(:get, mock_fetch_issues_url)
|
||||
.with(mock_headers).to_return(status: 200, body: mock_response.to_json)
|
||||
end
|
||||
|
||||
it 'returns the response' do
|
||||
expect(client.fetch_issues).to eq(mock_response)
|
||||
end
|
||||
|
||||
describe 'marking the issues as seen in the product' do
|
||||
let(:cache) { ::Gitlab::SetCache.new }
|
||||
let(:cache_key) do
|
||||
[
|
||||
:zentao_product_issues,
|
||||
OpenSSL::Digest::SHA256.hexdigest(zentao_integration.client_url),
|
||||
zentao_integration.zentao_product_xid
|
||||
].join(':')
|
||||
end
|
||||
|
||||
it 'adds issue ids to the cache' do
|
||||
expect { client.fetch_issues }.to change { cache.read(cache_key) }
|
||||
.from(be_empty)
|
||||
.to match_array(%w[bug-11 story-1])
|
||||
end
|
||||
|
||||
it 'does not add issue ids to the cache if max set size has been reached' do
|
||||
cache.write(cache_key, %w[foo bar])
|
||||
stub_const("#{described_class}::CACHE_MAX_SET_SIZE", 1)
|
||||
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.read(cache_key)).to match_array(%w[foo bar])
|
||||
end
|
||||
|
||||
it 'does not duplicate issue ids in the cache' do
|
||||
client.fetch_issues
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.read(cache_key)).to match_array(%w[bug-11 story-1])
|
||||
end
|
||||
|
||||
it 'touches the cache ttl every time issues are fetched' do
|
||||
fresh_ttl = 1.month.to_i
|
||||
|
||||
freeze_time do
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.ttl(cache_key)).to eq(fresh_ttl)
|
||||
end
|
||||
|
||||
travel_to(1.minute.from_now) do
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.ttl(cache_key)).to eq(fresh_ttl)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -109,9 +175,9 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
context 'with invalid id' do
|
||||
let(:invalid_ids) { ['story', 'story-', '-', '123', ''] }
|
||||
|
||||
it 'returns empty object' do
|
||||
it 'raises Error' do
|
||||
invalid_ids.each do |id|
|
||||
expect { integration.fetch_issue(id) }
|
||||
expect { client.fetch_issue(id) }
|
||||
.to raise_error(Gitlab::Zentao::Client::Error, 'invalid issue id')
|
||||
end
|
||||
end
|
||||
|
@ -120,12 +186,31 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
context 'with valid id' do
|
||||
let(:valid_ids) { %w[story-1 bug-23] }
|
||||
|
||||
it 'fetches current issue' do
|
||||
valid_ids.each do |id|
|
||||
WebMock.stub_request(:get, mock_fetch_issue_url(id))
|
||||
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
|
||||
context 'when issue has been seen on the index' do
|
||||
before do
|
||||
issues_body = { issues: valid_ids.map { { id: _1 } } }.to_json
|
||||
|
||||
expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id
|
||||
WebMock.stub_request(:get, mock_fetch_issues_url)
|
||||
.with(mock_headers).to_return(status: 200, body: issues_body)
|
||||
|
||||
client.fetch_issues
|
||||
end
|
||||
|
||||
it 'fetches the issue' do
|
||||
valid_ids.each do |id|
|
||||
WebMock.stub_request(:get, mock_fetch_issue_url(id))
|
||||
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
|
||||
|
||||
expect(client.fetch_issue(id).dig('issue', 'id')).to eq id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue has not been seen on the index' do
|
||||
it 'raises RequestError' do
|
||||
valid_ids.each do |id|
|
||||
expect { client.fetch_issue(id) }.to raise_error(Gitlab::Zentao::Client::RequestError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -135,7 +220,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
context 'api url' do
|
||||
shared_examples 'joins api_url correctly' do
|
||||
it 'verify url' do
|
||||
expect(integration.send(:url, "products/1").to_s)
|
||||
expect(client.send(:url, "products/1").to_s)
|
||||
.to eq("https://jihudemo.zentao.net/zentao/api.php/v1/products/1")
|
||||
end
|
||||
end
|
||||
|
@ -157,7 +242,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
let(:zentao_integration) { create(:zentao_integration, url: 'https://jihudemo.zentao.net') }
|
||||
|
||||
it 'joins url correctly' do
|
||||
expect(integration.send(:url, "products/1").to_s)
|
||||
expect(client.send(:url, "products/1").to_s)
|
||||
.to eq("https://jihudemo.zentao.net/api.php/v1/products/1")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,4 +81,24 @@ RSpec.describe Integrations::Zentao do
|
|||
expect(zentao_integration.help).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#client_url' do
|
||||
subject(:integration) { build(:zentao_integration, api_url: api_url, url: 'url').client_url }
|
||||
|
||||
context 'when api_url is set' do
|
||||
let(:api_url) { 'api_url' }
|
||||
|
||||
it 'returns the api_url' do
|
||||
is_expected.to eq(api_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when api_url is not set' do
|
||||
let(:api_url) { '' }
|
||||
|
||||
it 'returns the url' do
|
||||
is_expected.to eq('url')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -823,13 +823,21 @@ RSpec.describe Issue do
|
|||
end
|
||||
|
||||
describe '#to_branch_name exists ending with -index' do
|
||||
before do
|
||||
it 'returns #to_branch_name ending with max index + 1' do
|
||||
allow(repository).to receive(:branch_exists?).and_return(true)
|
||||
allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false)
|
||||
|
||||
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
|
||||
end
|
||||
|
||||
it 'returns #to_branch_name ending with max index + 1' do
|
||||
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
|
||||
context 'when branch name still exists after 5 attempts' do
|
||||
it 'returns #to_branch_name ending with random characters' do
|
||||
allow(repository).to receive(:branch_exists?).with(subject.to_branch_name).and_return(true)
|
||||
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(true)
|
||||
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\h{8}/).and_return(false)
|
||||
|
||||
expect(subject.suggested_branch_name).to match(/#{subject.to_branch_name}-\h{8}/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2625,7 +2625,7 @@ RSpec.describe Repository do
|
|||
end
|
||||
|
||||
shared_examples '#tree' do
|
||||
subject { repository.tree(sha, path, recursive: recursive, pagination_params: pagination_params) }
|
||||
subject { repository.tree(sha, path, recursive: recursive, skip_flat_paths: false, pagination_params: pagination_params) }
|
||||
|
||||
let(:sha) { :head }
|
||||
let(:path) { nil }
|
||||
|
|
|
@ -91,6 +91,45 @@ RSpec.describe Snippet do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'description validations' do
|
||||
let_it_be(:invalid_description) { 'a' * (described_class::DESCRIPTION_LENGTH_MAX * 2) }
|
||||
|
||||
context 'with existing snippets' do
|
||||
let(:snippet) { create(:personal_snippet, description: 'This is a valid content at the time of creation') }
|
||||
|
||||
it 'does not raise a validation error if the description is not changed' do
|
||||
snippet.title = 'new title'
|
||||
|
||||
expect(snippet).to be_valid
|
||||
end
|
||||
|
||||
it 'raises and error if the description is changed and the size is bigger than limit' do
|
||||
expect(snippet).to be_valid
|
||||
|
||||
snippet.description = invalid_description
|
||||
|
||||
expect(snippet).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'with new snippets' do
|
||||
it 'is valid when description is smaller than the limit' do
|
||||
snippet = build(:personal_snippet, description: 'Valid Desc')
|
||||
|
||||
expect(snippet).to be_valid
|
||||
end
|
||||
|
||||
it 'raises error when description is bigger than setting limit' do
|
||||
snippet = build(:personal_snippet, description: invalid_description)
|
||||
|
||||
aggregate_failures do
|
||||
expect(snippet).not_to be_valid
|
||||
expect(snippet.errors.messages_for(:description)).to include("is too long (2 MB). The maximum size is 1 MB.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
|
|
|
@ -12,29 +12,51 @@ RSpec.describe CommitPresenter do
|
|||
it { expect(presenter.web_path).to eq("/#{project.full_path}/-/commit/#{commit.sha}") }
|
||||
end
|
||||
|
||||
describe '#status_for' do
|
||||
subject { presenter.status_for('ref') }
|
||||
describe '#detailed_status_for' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
context 'when user can read_commit_status' do
|
||||
before do
|
||||
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(true)
|
||||
end
|
||||
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha, ref: 'ref') }
|
||||
|
||||
it 'returns commit status for ref' do
|
||||
pipeline = double
|
||||
status = double
|
||||
subject { presenter.detailed_status_for('ref')&.text }
|
||||
|
||||
expect(commit).to receive(:latest_pipeline).with('ref').and_return(pipeline)
|
||||
expect(pipeline).to receive(:detailed_status).with(user).and_return(status)
|
||||
|
||||
expect(subject).to eq(status)
|
||||
end
|
||||
where(:read_commit_status, :read_pipeline, :expected_result) do
|
||||
true | true | 'passed'
|
||||
true | false | nil
|
||||
false | true | nil
|
||||
false | false | nil
|
||||
end
|
||||
|
||||
context 'when user can not read_commit_status' do
|
||||
it 'is nil' do
|
||||
is_expected.to eq(nil)
|
||||
with_them do
|
||||
before do
|
||||
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(read_commit_status)
|
||||
allow(presenter).to receive(:can?).with(user, :read_pipeline, pipeline).and_return(read_pipeline)
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#status_for' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha) }
|
||||
|
||||
subject { presenter.status_for }
|
||||
|
||||
where(:read_commit_status, :read_pipeline, :expected_result) do
|
||||
true | true | 'success'
|
||||
true | false | nil
|
||||
false | true | nil
|
||||
false | false | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(read_commit_status)
|
||||
allow(presenter).to receive(:can?).with(user, :read_pipeline, pipeline).and_return(read_pipeline)
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,11 +6,16 @@ RSpec.describe 'getting incident timeline events' do
|
|||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:private_project) { create(:project, :private) }
|
||||
let_it_be(:issue) { create(:issue, project: private_project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:updated_by_user) { create(:user) }
|
||||
let_it_be(:incident) { create(:incident, project: project) }
|
||||
let_it_be(:another_incident) { create(:incident, project: project) }
|
||||
let_it_be(:promoted_from_note) { create(:note, project: project, noteable: incident) }
|
||||
let_it_be(:issue_url) { project_issue_url(private_project, issue) }
|
||||
let_it_be(:issue_ref) { "#{private_project.full_path}##{issue.iid}" }
|
||||
let_it_be(:issue_link) { %Q(<a href="#{issue_url}">#{issue_url}</a>) }
|
||||
|
||||
let_it_be(:timeline_event) do
|
||||
create(
|
||||
|
@ -18,7 +23,8 @@ RSpec.describe 'getting incident timeline events' do
|
|||
incident: incident,
|
||||
project: project,
|
||||
updated_by_user: updated_by_user,
|
||||
promoted_from_note: promoted_from_note
|
||||
promoted_from_note: promoted_from_note,
|
||||
note: "Referencing #{issue.to_reference(full: true)} - Full URL #{issue_url}"
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -89,7 +95,7 @@ RSpec.describe 'getting incident timeline events' do
|
|||
'title' => incident.title
|
||||
},
|
||||
'note' => timeline_event.note,
|
||||
'noteHtml' => timeline_event.note_html,
|
||||
'noteHtml' => "<p>Referencing #{issue_ref} - Full URL #{issue_link}</p>",
|
||||
'promotedFromNote' => {
|
||||
'id' => promoted_from_note.to_global_id.to_s,
|
||||
'body' => promoted_from_note.note
|
||||
|
|
|
@ -763,6 +763,96 @@ RSpec.describe API::Search do
|
|||
it_behaves_like 'pagination', scope: :commits, search: 'merge'
|
||||
|
||||
it_behaves_like 'ping counters', scope: :commits
|
||||
|
||||
describe 'pipeline visibility' do
|
||||
shared_examples 'pipeline information visible' do
|
||||
it 'contains status and last_pipeline' do
|
||||
request
|
||||
|
||||
expect(json_response[0]['status']).to eq 'success'
|
||||
expect(json_response[0]['last_pipeline']).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'pipeline information not visible' do
|
||||
it 'does not contain status and last_pipeline' do
|
||||
request
|
||||
|
||||
expect(json_response[0]['status']).to be_nil
|
||||
expect(json_response[0]['last_pipeline']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
let(:request) { get api(endpoint, user), params: { scope: 'commits', search: repo_project.commit.sha } }
|
||||
|
||||
before do
|
||||
create(:ci_pipeline, :success, project: repo_project, sha: repo_project.commit.sha)
|
||||
end
|
||||
|
||||
context 'with non public pipeline' do
|
||||
let_it_be(:repo_project) do
|
||||
create(:project, :public, :repository, public_builds: false, group: group)
|
||||
end
|
||||
|
||||
context 'user is project member with reporter role or above' do
|
||||
before do
|
||||
repo_project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
end
|
||||
|
||||
context 'user is project member with guest role' do
|
||||
before do
|
||||
repo_project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information not visible'
|
||||
end
|
||||
|
||||
context 'user is not project member' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it_behaves_like 'pipeline information not visible'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with public pipeline' do
|
||||
let_it_be(:repo_project) do
|
||||
create(:project, :public, :repository, public_builds: true, group: group)
|
||||
end
|
||||
|
||||
context 'user is project member with reporter role or above' do
|
||||
before do
|
||||
repo_project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
end
|
||||
|
||||
context 'user is project member with guest role' do
|
||||
before do
|
||||
repo_project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
end
|
||||
|
||||
context 'user is not project member' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
|
||||
context 'when CI/CD is set to only project members' do
|
||||
before do
|
||||
repo_project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information not visible'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for commits scope with project path as id' do
|
||||
|
|
|
@ -643,17 +643,17 @@ RSpec.describe 'Git HTTP requests' do
|
|||
end
|
||||
|
||||
context 'when username and password are provided' do
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the push attempt with personal access token error message' do
|
||||
it 'rejects the push attempt with generic error message' do
|
||||
upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -750,17 +750,17 @@ RSpec.describe 'Git HTTP requests' do
|
|||
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
|
||||
end
|
||||
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects pushes with personal access token error message' do
|
||||
it 'rejects pushes with generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -771,10 +771,10 @@ RSpec.describe 'Git HTTP requests' do
|
|||
.to receive(:login).and_return(nil)
|
||||
end
|
||||
|
||||
it 'does not display the personal access token error message' do
|
||||
it 'displays the generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1300,17 +1300,18 @@ RSpec.describe 'Git HTTP requests' do
|
|||
end
|
||||
|
||||
context 'when username and password are provided' do
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the push attempt with personal access token error message' do
|
||||
it 'rejects the push attempt with generic error message' do
|
||||
upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1381,17 +1382,17 @@ RSpec.describe 'Git HTTP requests' do
|
|||
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
|
||||
end
|
||||
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects pushes with personal access token error message' do
|
||||
it 'rejects pushes with generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1402,10 +1403,10 @@ RSpec.describe 'Git HTTP requests' do
|
|||
.to receive(:login).and_return(nil)
|
||||
end
|
||||
|
||||
it 'does not display the personal access token error message' do
|
||||
it 'returns a generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,6 +33,22 @@ RSpec.describe JwtController do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples "with invalid credentials" do
|
||||
it "returns a generic error message" do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(json_response).to eq(
|
||||
{
|
||||
"errors" => [{
|
||||
"code" => "UNAUTHORIZED",
|
||||
"message" => "HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/user/profile/account/two_factor_authentication#troubleshooting"
|
||||
}]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticating against container registry' do
|
||||
context 'existing service' do
|
||||
subject! { get '/jwt/auth', params: parameters }
|
||||
|
@ -51,10 +67,7 @@ RSpec.describe JwtController do
|
|||
context 'with blocked user' do
|
||||
let(:user) { create(:user, :blocked) }
|
||||
|
||||
it 'rejects the request as unauthorized' do
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('HTTP Basic: Access denied')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -154,10 +167,7 @@ RSpec.describe JwtController do
|
|||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
context 'without personal token' do
|
||||
it 'rejects the authorization attempt' do
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
|
||||
context 'with personal token' do
|
||||
|
@ -181,14 +191,10 @@ RSpec.describe JwtController do
|
|||
|
||||
context 'using invalid login' do
|
||||
let(:headers) { { authorization: credentials('invalid', 'password') } }
|
||||
let(:subject) { get '/jwt/auth', params: parameters, headers: headers }
|
||||
|
||||
context 'when internal auth is enabled' do
|
||||
it 'rejects the authorization attempt' do
|
||||
get '/jwt/auth', params: parameters, headers: headers
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
|
||||
context 'when internal auth is disabled' do
|
||||
|
@ -196,12 +202,7 @@ RSpec.describe JwtController do
|
|||
stub_application_setting(password_authentication_enabled_for_git: false)
|
||||
end
|
||||
|
||||
it 'rejects the authorization attempt with personal access token message' do
|
||||
get '/jwt/auth', params: parameters, headers: headers
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,6 +52,8 @@ module LoginHelpers
|
|||
visit new_admin_session_path
|
||||
fill_in 'user_password', with: user.password
|
||||
click_button 'Enter Admin Mode'
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
|
||||
|
|
|
@ -170,6 +170,17 @@ RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'rejected package download' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'process PyPI api request' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
|
@ -330,25 +341,25 @@ RSpec.shared_examples 'pypi file download endpoint' do
|
|||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
context 'with valid project' do
|
||||
where(:visibility_level, :user_role, :member, :user_token) do
|
||||
:public | :developer | true | true
|
||||
:public | :guest | true | true
|
||||
:public | :developer | true | false
|
||||
:public | :guest | true | false
|
||||
:public | :developer | false | true
|
||||
:public | :guest | false | true
|
||||
:public | :developer | false | false
|
||||
:public | :guest | false | false
|
||||
:public | :anonymous | false | true
|
||||
:private | :developer | true | true
|
||||
:private | :guest | true | true
|
||||
:private | :developer | true | false
|
||||
:private | :guest | true | false
|
||||
:private | :developer | false | true
|
||||
:private | :guest | false | true
|
||||
:private | :developer | false | false
|
||||
:private | :guest | false | false
|
||||
:private | :anonymous | false | true
|
||||
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | true | 'PyPI package download' | :success
|
||||
:public | :guest | true | true | 'PyPI package download' | :success
|
||||
:public | :developer | true | false | 'PyPI package download' | :success
|
||||
:public | :guest | true | false | 'PyPI package download' | :success
|
||||
:public | :developer | false | true | 'PyPI package download' | :success
|
||||
:public | :guest | false | true | 'PyPI package download' | :success
|
||||
:public | :developer | false | false | 'PyPI package download' | :success
|
||||
:public | :guest | false | false | 'PyPI package download' | :success
|
||||
:public | :anonymous | false | true | 'PyPI package download' | :success
|
||||
:private | :developer | true | true | 'PyPI package download' | :success
|
||||
:private | :guest | true | true | 'rejected package download' | :forbidden
|
||||
:private | :developer | true | false | 'rejected package download' | :unauthorized
|
||||
:private | :guest | true | false | 'rejected package download' | :unauthorized
|
||||
:private | :developer | false | true | 'rejected package download' | :not_found
|
||||
:private | :guest | false | true | 'rejected package download' | :not_found
|
||||
:private | :developer | false | false | 'rejected package download' | :unauthorized
|
||||
:private | :guest | false | false | 'rejected package download' | :unauthorized
|
||||
:private | :anonymous | false | true | 'rejected package download' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
@ -360,7 +371,7 @@ RSpec.shared_examples 'pypi file download endpoint' do
|
|||
group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
|
||||
end
|
||||
|
||||
it_behaves_like 'PyPI package download', params[:user_role], :success, params[:member]
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
36
spec/validators/bytesize_validator_spec.rb
Normal file
36
spec/validators/bytesize_validator_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BytesizeValidator do
|
||||
let(:model) do
|
||||
Class.new do
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :content
|
||||
alias_method :content_before_type_cast, :content
|
||||
|
||||
validates :content, bytesize: { maximum: -> { 7 } }
|
||||
end.new
|
||||
end
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:content, :validity, :errors) do
|
||||
'short' | true | {}
|
||||
'very long' | false | { content: ['is too long (9 Bytes). The maximum size is 7 Bytes.'] }
|
||||
'short😁' | false | { content: ['is too long (9 Bytes). The maximum size is 7 Bytes.'] }
|
||||
'short⇏' | false | { content: ['is too long (8 Bytes). The maximum size is 7 Bytes.'] }
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
model.content = content
|
||||
model.validate
|
||||
end
|
||||
|
||||
it { expect(model.valid?).to eq(validity) }
|
||||
it { expect(model.errors.messages).to eq(errors) }
|
||||
end
|
||||
end
|
|
@ -47,13 +47,12 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
|
|||
|
||||
context 'with ci status' do
|
||||
let(:ref) { 'master' }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
allow(view).to receive(:current_user).and_return(user)
|
||||
|
||||
project.add_developer(user)
|
||||
|
||||
create(
|
||||
:ci_empty_pipeline,
|
||||
ref: 'master',
|
||||
|
@ -80,18 +79,32 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
|
|||
end
|
||||
|
||||
context 'when pipelines are enabled' do
|
||||
before do
|
||||
allow(project).to receive(:builds_enabled?).and_return(true)
|
||||
context 'when user has access' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'displays a ci status icon' do
|
||||
render partial: template, formats: :html, locals: {
|
||||
project: project,
|
||||
ref: ref,
|
||||
commit: commit
|
||||
}
|
||||
|
||||
expect(rendered).to have_css('.ci-status-link')
|
||||
end
|
||||
end
|
||||
|
||||
it 'does display a ci status icon when pipelines are enabled' do
|
||||
render partial: template, formats: :html, locals: {
|
||||
project: project,
|
||||
ref: ref,
|
||||
commit: commit
|
||||
}
|
||||
context 'when user does not have access' do
|
||||
it 'does not display a ci status icon' do
|
||||
render partial: template, formats: :html, locals: {
|
||||
project: project,
|
||||
ref: ref,
|
||||
commit: commit
|
||||
}
|
||||
|
||||
expect(rendered).to have_css('.ci-status-link')
|
||||
expect(rendered).not_to have_css('.ci-status-link')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
51
yarn.lock
51
yarn.lock
|
@ -979,6 +979,14 @@
|
|||
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f"
|
||||
integrity sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==
|
||||
|
||||
"@codesandbox/sandpack-client@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@codesandbox/sandpack-client/-/sandpack-client-1.2.2.tgz#e0b79c52dcbc0b622f93527dc9ff3b163467e14a"
|
||||
integrity sha512-sTPQVS7mzpEm2ttpHFFSqkGd1A1tBZn7UTZwIjBNCXKHywrt9o7MyrdhUuS03J7MyXN+HSJ55Vz+OGD1Wv4ejQ==
|
||||
dependencies:
|
||||
codesandbox-import-utils "^1.2.3"
|
||||
lodash.isequal "^4.5.0"
|
||||
|
||||
"@csstools/selector-specificity@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87"
|
||||
|
@ -2936,9 +2944,9 @@ binary-extensions@^2.0.0:
|
|||
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
||||
|
||||
binaryextensions@2:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
|
||||
integrity sha512-XBaoWE9RW8pPdPQNibZsW2zh8TW6gcarXp1FZPwT8Uop8ScSNldJEWf2k9l3HeTqdrEwsOsFcq74RiJECW34yA==
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
|
||||
integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
|
||||
|
||||
bluebird@^3.1.1, bluebird@^3.5.5:
|
||||
version "3.5.5"
|
||||
|
@ -3456,18 +3464,18 @@ codesandbox-api@0.0.23:
|
|||
resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.23.tgz#bf650a21b5f3c2369e03f0c19d10b4e2ba255b4f"
|
||||
integrity sha512-fFGBkIghDkQILh7iHYlpZU5sfWncCDb92FQSFE4rR3VBcTfUsD5VZgpQi+JjZQuwWIdfl4cOhcIFrUYwshUezA==
|
||||
|
||||
codesandbox-import-util-types@^1.2.11:
|
||||
version "1.2.11"
|
||||
resolved "https://registry.yarnpkg.com/codesandbox-import-util-types/-/codesandbox-import-util-types-1.2.11.tgz#68e812f21d6b309e9a52eec5cf027c3e63b4c703"
|
||||
integrity sha512-n1PC/OQ0tcD9o6N5TStBB/A7tKOggUjuhnNxUU5GnVol8vmKMMLvmC6tK+8iDovQb2X2+xoDCBnl5BBgZ5OcIQ==
|
||||
codesandbox-import-util-types@^1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/codesandbox-import-util-types/-/codesandbox-import-util-types-1.3.7.tgz#7a6097e248a75424d13b06b74368cd76bd2b3e10"
|
||||
integrity sha512-8oP3emA0jyEuVOM2FBTpo/AF4C9vxHn14saVWZf2CQ/QhMtonBlNPE98ElrHkW+PFNXiO7Ad52Qr73b03n8qlA==
|
||||
|
||||
codesandbox-import-utils@^1.2.3:
|
||||
version "1.2.11"
|
||||
resolved "https://registry.yarnpkg.com/codesandbox-import-utils/-/codesandbox-import-utils-1.2.11.tgz#b88423a4a7c785175c784c84e87f5950820280e1"
|
||||
integrity sha512-KPuf7tR/SMPSRfqjWbTrYvIaW6Yt9Ajt/1FB64RsOv4BLjBNo6CwLCCPoRHYcrAKSafpWkghTZ2Bffyz7EX7AA==
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/codesandbox-import-utils/-/codesandbox-import-utils-1.3.8.tgz#5576786439c5f37ebd3fee5751e06027a1edef84"
|
||||
integrity sha512-S12zO49QEkldoYLGh5KbkHRLOacg5BCNTue2vlyZXSpuK3oQdArwC/G1hCLKryV460bW3Ecn5xdkpfkUcFeOwQ==
|
||||
dependencies:
|
||||
codesandbox-import-util-types "^1.2.11"
|
||||
istextorbinary "^2.2.1"
|
||||
codesandbox-import-util-types "^1.3.7"
|
||||
istextorbinary "2.2.1"
|
||||
lz-string "^1.4.4"
|
||||
|
||||
collect-v8-coverage@^1.0.0:
|
||||
|
@ -6980,7 +6988,7 @@ istanbul-reports@^3.0.0, istanbul-reports@^3.1.3:
|
|||
html-escaper "^2.0.0"
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
istextorbinary@^2.2.1:
|
||||
istextorbinary@2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
|
||||
integrity sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==
|
||||
|
@ -7935,7 +7943,7 @@ lru-cache@^6.0.0:
|
|||
lz-string@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
|
||||
integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=
|
||||
integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
|
||||
|
||||
make-dir@^2.0.0:
|
||||
version "2.1.0"
|
||||
|
@ -10720,15 +10728,6 @@ slice-ansi@^4.0.0:
|
|||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
smooshpack@^0.0.62:
|
||||
version "0.0.62"
|
||||
resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.62.tgz#cb31b9f808f73de3146b050f84d044eb353b5503"
|
||||
integrity sha512-lFuJV2f504/U78sifWy0V2FyoE/8mTgOXM4DL918ncNxAxbtu236XSCLAH3SQwXZWn0JdmRnWs/XU4+sIUVVmQ==
|
||||
dependencies:
|
||||
codesandbox-api "0.0.23"
|
||||
codesandbox-import-utils "^1.2.3"
|
||||
lodash.isequal "^4.5.0"
|
||||
|
||||
snapdragon-node@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
|
||||
|
@ -11300,9 +11299,9 @@ text-table@^0.2.0:
|
|||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
||||
textextensions@2:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
|
||||
integrity sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA==
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4"
|
||||
integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==
|
||||
|
||||
three-orbit-controls@^82.1.0:
|
||||
version "82.1.0"
|
||||
|
|
Loading…
Reference in a new issue