Update upstream source from tag 'upstream/14.2.6+ds1'
Update to upstream version '14.2.6+ds1'
with Debian dir 7d2b7c02e9
This commit is contained in:
commit
43416ee97d
76 changed files with 859 additions and 106 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -2,6 +2,24 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 14.2.6 (2021-10-28)
|
||||||
|
|
||||||
|
### Security (13 changes)
|
||||||
|
|
||||||
|
- [Highlight usage of unicode bidi characters](gitlab-org/security/gitlab@18a768bb3cd19b6dc780bb85d91a93605ec8aa4f) ([merge request](gitlab-org/security/gitlab!1939))
|
||||||
|
- [Fix dompurify.js to prevent path traversal attacks](gitlab-org/security/gitlab@cfd7c715162c22060b9b80268ef501a9e604421a) ([merge request](gitlab-org/security/gitlab!1931))
|
||||||
|
- [Refresh authorizations on transfer of groups having project shares](gitlab-org/security/gitlab@3fc08eb869156a090b015e78da79c8ced16a7162) ([merge request](gitlab-org/security/gitlab!1918))
|
||||||
|
- [Do not allow Applications API to create apps with blank scopes](gitlab-org/security/gitlab@c4ffc8c0ee5356bcb9b76dbfa92517589b4225a8) ([merge request](gitlab-org/security/gitlab!1924))
|
||||||
|
- [Don't allow author to resolve discussions when MR is locked via GraphQL](gitlab-org/security/gitlab@fe2d0b6f250b60619da97f162c93c9e645daf4af) ([merge request](gitlab-org/security/gitlab!1921))
|
||||||
|
- [Workhorse: Allow uploading only a single file](gitlab-org/security/gitlab@89b04599592b7dfc0e4883cfde5d3ecd9ea855b2) ([merge request](gitlab-org/security/gitlab!1915))
|
||||||
|
- [Group owners should see SCIM token only once](gitlab-org/security/gitlab@d52c1e41f38039db075a7a3418b8eb9ed8474c2a) ([merge request](gitlab-org/security/gitlab!1908)) **GitLab Enterprise Edition**
|
||||||
|
- [Respect visibility level settings when updating project via API](gitlab-org/security/gitlab@3051d6a00d1a56133a77ecd24313bafb4565d576) ([merge request](gitlab-org/security/gitlab!1905))
|
||||||
|
- [Avoid decoding the whole tiff image on isTIFF check](gitlab-org/security/gitlab@bab7f45def8fc81fe4b0961a21b4c90a60358ff9) ([merge request](gitlab-org/security/gitlab!1901))
|
||||||
|
- [Adding a '[redacted]' to mask private email addresses](gitlab-org/security/gitlab@8eb9749f40b87b9b49b034bceb263219a4d3b114) ([merge request](gitlab-org/security/gitlab!1895))
|
||||||
|
- [Do not display the root password by default](gitlab-org/security/gitlab@4ccf08b6645b9f616657edd266d9d31e3602d170) ([merge request](gitlab-org/security/gitlab!1802))
|
||||||
|
- [Set PipelineSchedules to inactive](gitlab-org/security/gitlab@ebee16945325d22ceb5c07b7ba48df6fd0b2f067) ([merge request](gitlab-org/security/gitlab!1878))
|
||||||
|
- [Remove external_webhook_token from exported project](gitlab-org/security/gitlab@f3ef12185902f3ed5c9d62ffce07418fd704a753) ([merge request](gitlab-org/security/gitlab!1865))
|
||||||
|
|
||||||
## 14.2.5 (2021-09-30)
|
## 14.2.5 (2021-09-30)
|
||||||
|
|
||||||
### Security (28 changes)
|
### Security (28 changes)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
14.2.5
|
14.2.6
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
14.2.5
|
14.2.6
|
|
@ -1,5 +1,5 @@
|
||||||
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
|
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
|
||||||
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
|
import { getNormalizedURL, getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
// Safely allow SVG <use> tags
|
// Safely allow SVG <use> tags
|
||||||
|
@ -11,12 +11,14 @@ const defaultConfig = {
|
||||||
|
|
||||||
// Only icons urls from `gon` are allowed
|
// Only icons urls from `gon` are allowed
|
||||||
const getAllowedIconUrls = (gon = window.gon) =>
|
const getAllowedIconUrls = (gon = window.gon) =>
|
||||||
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
|
[gon.sprite_file_icons, gon.sprite_icons]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((path) => relativePathToAbsolute(path, getBaseURL()));
|
||||||
|
|
||||||
const isUrlAllowed = (url) => getAllowedIconUrls().some((allowedUrl) => url.startsWith(allowedUrl));
|
const isUrlAllowed = (url) =>
|
||||||
|
getAllowedIconUrls().some((allowedUrl) => getNormalizedURL(url).startsWith(allowedUrl));
|
||||||
|
|
||||||
const isHrefSafe = (url) =>
|
const isHrefSafe = (url) => url.match(/^#/) || isUrlAllowed(url);
|
||||||
isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
|
|
||||||
|
|
||||||
const removeUnsafeHref = (node, attr) => {
|
const removeUnsafeHref = (node, attr) => {
|
||||||
if (!node.hasAttribute(attr)) {
|
if (!node.hasAttribute(attr)) {
|
||||||
|
@ -36,13 +38,14 @@ const removeUnsafeHref = (node, attr) => {
|
||||||
* <use href="/assets/icons-xxx.svg#icon_name"></use>
|
* <use href="/assets/icons-xxx.svg#icon_name"></use>
|
||||||
* </svg>
|
* </svg>
|
||||||
*
|
*
|
||||||
|
* It validates both href & xlink:href attributes.
|
||||||
|
* Note that `xlink:href` is deprecated, but still in use
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
|
||||||
|
*
|
||||||
* @param {Object} node - Node to sanitize
|
* @param {Object} node - Node to sanitize
|
||||||
*/
|
*/
|
||||||
const sanitizeSvgIcon = (node) => {
|
const sanitizeSvgIcon = (node) => {
|
||||||
removeUnsafeHref(node, 'href');
|
removeUnsafeHref(node, 'href');
|
||||||
|
|
||||||
// Note: `xlink:href` is deprecated, but still in use
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
|
|
||||||
removeUnsafeHref(node, 'xlink:href');
|
removeUnsafeHref(node, 'xlink:href');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -397,6 +397,24 @@ export function isSafeURL(url) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a normalized url
|
||||||
|
*
|
||||||
|
* https://gitlab.com/foo/../baz => https://gitlab.com/baz
|
||||||
|
*
|
||||||
|
* @param {String} url - URL to be transformed
|
||||||
|
* @param {String?} baseUrl - current base URL
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
export const getNormalizedURL = (url, baseUrl) => {
|
||||||
|
const base = baseUrl || getBaseURL();
|
||||||
|
try {
|
||||||
|
return new URL(url, base).href;
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function getWebSocketProtocol() {
|
export function getWebSocketProtocol() {
|
||||||
return window.location.protocol.replace('http', 'ws');
|
return window.location.protocol.replace('http', 'ws');
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@ export default {
|
||||||
securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
|
securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
|
||||||
snippetsLabel: s__('ProjectSettings|Snippets'),
|
snippetsLabel: s__('ProjectSettings|Snippets'),
|
||||||
wikiLabel: s__('ProjectSettings|Wiki'),
|
wikiLabel: s__('ProjectSettings|Wiki'),
|
||||||
|
pucWarningLabel: s__('ProjectSettings|Warn about Potentially Unwanted Characters'),
|
||||||
|
pucWarningHelpText: s__(
|
||||||
|
'ProjectSettings|Highlight the usage of hidden unicode characters. These have innocent uses for right-to-left languages, but can also be used in potential exploits.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
|
@ -178,6 +182,7 @@ export default {
|
||||||
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
|
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
|
||||||
operationsAccessLevel: featureAccessLevel.EVERYONE,
|
operationsAccessLevel: featureAccessLevel.EVERYONE,
|
||||||
containerRegistryAccessLevel: featureAccessLevel.EVERYONE,
|
containerRegistryAccessLevel: featureAccessLevel.EVERYONE,
|
||||||
|
warnAboutPotentiallyUnwantedCharacters: true,
|
||||||
lfsEnabled: true,
|
lfsEnabled: true,
|
||||||
requestAccessEnabled: true,
|
requestAccessEnabled: true,
|
||||||
highlightChangesClass: false,
|
highlightChangesClass: false,
|
||||||
|
@ -752,5 +757,19 @@ export default {
|
||||||
}}</template>
|
}}</template>
|
||||||
</gl-form-checkbox>
|
</gl-form-checkbox>
|
||||||
</project-setting-row>
|
</project-setting-row>
|
||||||
|
<project-setting-row class="gl-mb-5">
|
||||||
|
<input
|
||||||
|
:value="warnAboutPotentiallyUnwantedCharacters"
|
||||||
|
type="hidden"
|
||||||
|
name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"
|
||||||
|
/>
|
||||||
|
<gl-form-checkbox
|
||||||
|
v-model="warnAboutPotentiallyUnwantedCharacters"
|
||||||
|
name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"
|
||||||
|
>
|
||||||
|
{{ $options.i18n.pucWarningLabel }}
|
||||||
|
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
|
||||||
|
</gl-form-checkbox>
|
||||||
|
</project-setting-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -66,7 +66,13 @@ export default {
|
||||||
data-qa-selector="clone_button"
|
data-qa-selector="clone_button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<snippet-blob v-for="blob in blobs" :key="blob.path" :snippet="snippet" :blob="blob" />
|
<snippet-blob
|
||||||
|
v-for="blob in blobs"
|
||||||
|
:key="blob.path"
|
||||||
|
:snippet="snippet"
|
||||||
|
:blob="blob"
|
||||||
|
class="project-highlight-puc"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -85,3 +85,9 @@
|
||||||
td.line-numbers {
|
td.line-numbers {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-highlight-puc .unicode-bidi::before {
|
||||||
|
content: '<EFBFBD>';
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline wavy $red-500;
|
||||||
|
}
|
||||||
|
|
|
@ -398,6 +398,7 @@ class ProjectsController < Projects::ApplicationController
|
||||||
show_default_award_emojis
|
show_default_award_emojis
|
||||||
squash_option
|
squash_option
|
||||||
mr_default_target_self
|
mr_default_target_self
|
||||||
|
warn_about_potentially_unwanted_characters
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ module Mutations
|
||||||
argument :severity, Types::IssuableSeverityEnum, required: true,
|
argument :severity, Types::IssuableSeverityEnum, required: true,
|
||||||
description: 'Set the incident severity level.'
|
description: 'Set the incident severity level.'
|
||||||
|
|
||||||
|
authorize :admin_issue
|
||||||
|
|
||||||
def resolve(project_path:, iid:, severity:)
|
def resolve(project_path:, iid:, severity:)
|
||||||
issue = authorized_find!(project_path: project_path, iid: iid)
|
issue = authorized_find!(project_path: project_path, iid: iid)
|
||||||
project = issue.project
|
project = issue.project
|
||||||
|
|
|
@ -377,6 +377,12 @@ module ProjectsHelper
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_classes(project)
|
||||||
|
return "project-highlight-puc" if project.warn_about_potentially_unwanted_characters?
|
||||||
|
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def tab_ability_map
|
def tab_ability_map
|
||||||
|
@ -533,6 +539,7 @@ module ProjectsHelper
|
||||||
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
|
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
|
||||||
operationsAccessLevel: feature.operations_access_level,
|
operationsAccessLevel: feature.operations_access_level,
|
||||||
showDefaultAwardEmojis: project.show_default_award_emojis?,
|
showDefaultAwardEmojis: project.show_default_award_emojis?,
|
||||||
|
warnAboutPotentiallyUnwantedCharacters: project.warn_about_potentially_unwanted_characters?,
|
||||||
securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
|
securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
|
||||||
containerRegistryAccessLevel: feature.container_registry_access_level
|
containerRegistryAccessLevel: feature.container_registry_access_level
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,8 +81,7 @@ module ResolvableDiscussion
|
||||||
return false unless current_user
|
return false unless current_user
|
||||||
return false unless resolvable?
|
return false unless resolvable?
|
||||||
|
|
||||||
current_user == self.noteable.try(:author) ||
|
current_user.can?(:resolve_note, self.noteable)
|
||||||
current_user.can?(:resolve_note, self.project)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve!(current_user)
|
def resolve!(current_user)
|
||||||
|
|
|
@ -26,6 +26,8 @@ class Namespace < ApplicationRecord
|
||||||
SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
|
SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
|
||||||
URL_MAX_LENGTH = 255
|
URL_MAX_LENGTH = 255
|
||||||
|
|
||||||
|
PATH_TRAILING_VIOLATIONS = %w[.git .atom .].freeze
|
||||||
|
|
||||||
cache_markdown_field :description, pipeline: :description
|
cache_markdown_field :description, pipeline: :description
|
||||||
|
|
||||||
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||||
|
@ -162,9 +164,14 @@ class Namespace < ApplicationRecord
|
||||||
# Remove everything that's not in the list of allowed characters.
|
# Remove everything that's not in the list of allowed characters.
|
||||||
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
|
path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
|
||||||
# Remove trailing violations ('.atom', '.git', or '.')
|
# Remove trailing violations ('.atom', '.git', or '.')
|
||||||
path.gsub!(/(\.atom|\.git|\.)*\z/, "")
|
loop do
|
||||||
|
orig = path
|
||||||
|
PATH_TRAILING_VIOLATIONS.each { |ext| path = path.chomp(ext) }
|
||||||
|
break if orig == path
|
||||||
|
end
|
||||||
|
|
||||||
# Remove leading violations ('-')
|
# Remove leading violations ('-')
|
||||||
path.gsub!(/\A\-+/, "")
|
path.gsub!(/\A\-+/, "")
|
||||||
|
|
||||||
# Users with the great usernames of "." or ".." would end up with a blank username.
|
# Users with the great usernames of "." or ".." would end up with a blank username.
|
||||||
# Work around that by setting their username to "blank", followed by a counter.
|
# Work around that by setting their username to "blank", followed by a counter.
|
||||||
|
|
|
@ -412,8 +412,8 @@ class Project < ApplicationRecord
|
||||||
:container_registry_access_level, :container_registry_enabled?,
|
:container_registry_access_level, :container_registry_enabled?,
|
||||||
to: :project_feature, allow_nil: true
|
to: :project_feature, allow_nil: true
|
||||||
alias_method :container_registry_enabled, :container_registry_enabled?
|
alias_method :container_registry_enabled, :container_registry_enabled?
|
||||||
delegate :show_default_award_emojis, :show_default_award_emojis=,
|
delegate :show_default_award_emojis, :show_default_award_emojis=, :show_default_award_emojis?,
|
||||||
:show_default_award_emojis?,
|
:warn_about_potentially_unwanted_characters, :warn_about_potentially_unwanted_characters=, :warn_about_potentially_unwanted_characters?,
|
||||||
to: :project_setting, allow_nil: true
|
to: :project_setting, allow_nil: true
|
||||||
delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
|
delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
|
||||||
prefix: :import, to: :import_state, allow_nil: true
|
prefix: :import, to: :import_state, allow_nil: true
|
||||||
|
@ -2669,8 +2669,23 @@ class Project < ApplicationRecord
|
||||||
ci_cd_settings.group_runners_enabled?
|
ci_cd_settings.group_runners_enabled?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visible_group_links(for_user:)
|
||||||
|
user = for_user
|
||||||
|
links = project_group_links_with_preload
|
||||||
|
user.max_member_access_for_group_ids(links.map(&:group_id)) if user && links.any?
|
||||||
|
|
||||||
|
DeclarativePolicy.user_scope do
|
||||||
|
links.select { Ability.allowed?(user, :read_group, _1.group) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# overridden in EE
|
||||||
|
def project_group_links_with_preload
|
||||||
|
project_group_links
|
||||||
|
end
|
||||||
|
|
||||||
def find_integration(integrations, name)
|
def find_integration(integrations, name)
|
||||||
integrations.find { _1.to_param == name }
|
integrations.find { _1.to_param == name }
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,7 @@ class ProjectGroupLink < ApplicationRecord
|
||||||
validate :different_group
|
validate :different_group
|
||||||
|
|
||||||
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
|
scope :non_guests, -> { where('group_access > ?', Gitlab::Access::GUEST) }
|
||||||
|
scope :in_group, -> (group_ids) { where(group_id: group_ids) }
|
||||||
|
|
||||||
alias_method :shared_with_group, :group
|
alias_method :shared_with_group, :group
|
||||||
|
|
||||||
|
|
|
@ -1459,7 +1459,7 @@ class User < ApplicationRecord
|
||||||
name: name,
|
name: name,
|
||||||
username: username,
|
username: username,
|
||||||
avatar_url: avatar_url(only_path: false),
|
avatar_url: avatar_url(only_path: false),
|
||||||
email: email
|
email: public_email.presence || _('[REDACTED]')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ class IssuablePolicy < BasePolicy
|
||||||
@user && @subject.assignee_or_author?(@user)
|
@user && @subject.assignee_or_author?(@user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
condition(:is_author) { @subject&.author == @user }
|
||||||
|
|
||||||
rule { can?(:guest_access) & assignee_or_author }.policy do
|
rule { can?(:guest_access) & assignee_or_author }.policy do
|
||||||
enable :read_issue
|
enable :read_issue
|
||||||
enable :update_issue
|
enable :update_issue
|
||||||
|
@ -20,6 +22,10 @@ class IssuablePolicy < BasePolicy
|
||||||
enable :reopen_merge_request
|
enable :reopen_merge_request
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { is_author }.policy do
|
||||||
|
enable :resolve_note
|
||||||
|
end
|
||||||
|
|
||||||
rule { locked & ~is_project_member }.policy do
|
rule { locked & ~is_project_member }.policy do
|
||||||
prevent :create_note
|
prevent :create_note
|
||||||
prevent :admin_note
|
prevent :admin_note
|
||||||
|
|
|
@ -221,6 +221,7 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :set_note_created_at
|
enable :set_note_created_at
|
||||||
enable :set_emails_disabled
|
enable :set_emails_disabled
|
||||||
enable :set_show_default_award_emojis
|
enable :set_show_default_award_emojis
|
||||||
|
enable :set_warn_about_potentially_unwanted_characters
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { can?(:guest_access) }.policy do
|
rule { can?(:guest_access) }.policy do
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module UpdateVisibilityLevel
|
module UpdateVisibilityLevel
|
||||||
|
# check that user is allowed to set specified visibility_level
|
||||||
def valid_visibility_level_change?(target, new_visibility)
|
def valid_visibility_level_change?(target, new_visibility)
|
||||||
# check that user is allowed to set specified visibility_level
|
return true unless new_visibility
|
||||||
if new_visibility && new_visibility.to_i != target.visibility_level
|
|
||||||
unless can?(current_user, :change_visibility_level, target) &&
|
|
||||||
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
|
|
||||||
|
|
||||||
deny_visibility_level(target, new_visibility)
|
new_visibility_level = Gitlab::VisibilityLevel.level_value(new_visibility)
|
||||||
|
|
||||||
|
if new_visibility_level != target.visibility_level_value
|
||||||
|
unless can?(current_user, :change_visibility_level, target) &&
|
||||||
|
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility_level)
|
||||||
|
|
||||||
|
deny_visibility_level(target, new_visibility_level)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -177,6 +177,14 @@ module Groups
|
||||||
|
|
||||||
# schedule refreshing projects for all the members of the group
|
# schedule refreshing projects for all the members of the group
|
||||||
@group.refresh_members_authorized_projects
|
@group.refresh_members_authorized_projects
|
||||||
|
|
||||||
|
# When a group is transferred, it also affects who gets access to the projects shared to
|
||||||
|
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
|
||||||
|
project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id))
|
||||||
|
|
||||||
|
project_group_shares_within_the_hierarchy.find_each do |project_group_link|
|
||||||
|
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def raise_transfer_error(message)
|
def raise_transfer_error(message)
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Groups
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
return false unless valid_visibility_level_change?(group, params[:visibility_level])
|
return false unless valid_visibility_level_change?(group, group.visibility_attribute_value(params))
|
||||||
|
|
||||||
return false unless valid_share_with_group_lock_change?
|
return false unless valid_share_with_group_lock_change?
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ module Groups
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_update
|
def after_update
|
||||||
if group.previous_changes.include?(:visibility_level) && group.private?
|
if group.previous_changes.include?(group.visibility_level_field) && group.private?
|
||||||
# don't enqueue immediately to prevent todos removal in case of a mistake
|
# don't enqueue immediately to prevent todos removal in case of a mistake
|
||||||
TodosDestroyer::GroupPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, group.id)
|
TodosDestroyer::GroupPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, group.id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -139,6 +139,7 @@ class IssuableBaseService < ::BaseProjectService
|
||||||
def filter_severity(issuable)
|
def filter_severity(issuable)
|
||||||
severity = params.delete(:severity)
|
severity = params.delete(:severity)
|
||||||
return unless severity && issuable.supports_severity?
|
return unless severity && issuable.supports_severity?
|
||||||
|
return unless can_admin_issuable?(issuable)
|
||||||
|
|
||||||
severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity)
|
severity = IssuableSeverity::DEFAULT unless IssuableSeverity.severities.key?(severity)
|
||||||
return if severity == issuable.severity
|
return if severity == issuable.severity
|
||||||
|
|
|
@ -49,7 +49,7 @@ module Projects
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate!
|
def validate!
|
||||||
unless valid_visibility_level_change?(project, params[:visibility_level])
|
unless valid_visibility_level_change?(project, project.visibility_attribute_value(params))
|
||||||
raise ValidationError, s_('UpdateProject|New visibility level not allowed!')
|
raise ValidationError, s_('UpdateProject|New visibility level not allowed!')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
- display_subscription_banner!
|
- display_subscription_banner!
|
||||||
- display_namespace_storage_limit_alert!
|
- display_namespace_storage_limit_alert!
|
||||||
- @left_sidebar = true
|
- @left_sidebar = true
|
||||||
|
- @content_class = [@content_class, project_classes(@project)].compact.join(" ")
|
||||||
|
|
||||||
- content_for :project_javascripts do
|
- content_for :project_javascripts do
|
||||||
- project = @target_project || @project
|
- project = @target_project || @project
|
||||||
|
|
|
@ -176,8 +176,10 @@ production: &base
|
||||||
## Application settings cache expiry in seconds (default: 60)
|
## Application settings cache expiry in seconds (default: 60)
|
||||||
# application_settings_cache_seconds: 60
|
# application_settings_cache_seconds: 60
|
||||||
|
|
||||||
## Print initial root password to stdout during initialization (default: true)
|
## Print initial root password to stdout during initialization (default: false)
|
||||||
# display_initial_root_password: true
|
# WARNING: setting this to true means that the root password will be printed in
|
||||||
|
# plaintext. This can be a security risk.
|
||||||
|
# display_initial_root_password: false
|
||||||
|
|
||||||
## Reply by email
|
## Reply by email
|
||||||
# Allow users to comment on issues and merge requests by replying to notification emails.
|
# Allow users to comment on issues and merge requests by replying to notification emails.
|
||||||
|
|
|
@ -217,8 +217,7 @@ Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config'
|
||||||
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
|
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
|
||||||
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
|
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
|
||||||
Settings.gitlab['max_request_duration_seconds'] ||= 57
|
Settings.gitlab['max_request_duration_seconds'] ||= 57
|
||||||
|
Settings.gitlab['display_initial_root_password'] = false if Settings.gitlab['display_initial_root_password'].nil?
|
||||||
Settings.gitlab['display_initial_root_password'] = true if Settings.gitlab['display_initial_root_password'].nil?
|
|
||||||
|
|
||||||
Gitlab.ee do
|
Gitlab.ee do
|
||||||
Settings.gitlab['mirror_max_delay'] ||= 300
|
Settings.gitlab['mirror_max_delay'] ||= 300
|
||||||
|
|
|
@ -26,7 +26,7 @@ if user.persisted?
|
||||||
if ::Settings.gitlab['display_initial_root_password']
|
if ::Settings.gitlab['display_initial_root_password']
|
||||||
puts "password: #{user_args[:password]}".color(:green)
|
puts "password: #{user_args[:password]}".color(:green)
|
||||||
else
|
else
|
||||||
puts "password: *** - You opted not to display initial root password to STDOUT."
|
puts "password: ******".color(:green)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
puts "password: You'll be prompted to create one on your first visit.".color(:green)
|
puts "password: You'll be prompted to create one on your first visit.".color(:green)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||||
|
# for more information on how to write migrations for GitLab.
|
||||||
|
|
||||||
|
class AddWarnAboutPotentiallyUnwantedCharactersToProjectSettings < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def up
|
||||||
|
with_lock_retries do
|
||||||
|
add_column :project_settings, :warn_about_potentially_unwanted_characters, :boolean, null: false, default: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
with_lock_retries do
|
||||||
|
remove_column :project_settings, :warn_about_potentially_unwanted_characters
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
1
db/schema_migrations/20210929144453
Normal file
1
db/schema_migrations/20210929144453
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0f808c27d19e6a38d4aa31f2dd820fe226681af84e05c4af47213409b2043e5a
|
|
@ -17302,6 +17302,7 @@ CREATE TABLE project_settings (
|
||||||
cve_id_request_enabled boolean DEFAULT true NOT NULL,
|
cve_id_request_enabled boolean DEFAULT true NOT NULL,
|
||||||
mr_default_target_self boolean DEFAULT false NOT NULL,
|
mr_default_target_self boolean DEFAULT false NOT NULL,
|
||||||
previous_default_branch text,
|
previous_default_branch text,
|
||||||
|
warn_about_potentially_unwanted_characters boolean DEFAULT true NOT NULL,
|
||||||
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
|
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
|
||||||
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL))
|
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL))
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@ module API
|
||||||
params do
|
params do
|
||||||
requires :name, type: String, desc: 'Application name'
|
requires :name, type: String, desc: 'Application name'
|
||||||
requires :redirect_uri, type: String, desc: 'Application redirect URI'
|
requires :redirect_uri, type: String, desc: 'Application redirect URI'
|
||||||
requires :scopes, type: String, desc: 'Application scopes'
|
requires :scopes, type: String, desc: 'Application scopes', allow_blank: false
|
||||||
|
|
||||||
optional :confidential, type: Boolean, default: true,
|
optional :confidential, type: Boolean, default: true,
|
||||||
desc: 'Application will be used where the client secret is confidential'
|
desc: 'Application will be used where the client secret is confidential'
|
||||||
|
|
|
@ -100,7 +100,9 @@ module API
|
||||||
expose :build_coverage_regex
|
expose :build_coverage_regex
|
||||||
expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
|
expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
|
||||||
expose :shared_with_groups do |project, options|
|
expose :shared_with_groups do |project, options|
|
||||||
SharedGroupWithProject.represent(project.project_group_links, options)
|
user = options[:current_user]
|
||||||
|
|
||||||
|
SharedGroupWithProject.represent(project.visible_group_links(for_user: user), options)
|
||||||
end
|
end
|
||||||
expose :only_allow_merge_if_pipeline_succeeds
|
expose :only_allow_merge_if_pipeline_succeeds
|
||||||
expose :allow_merge_on_skipped_pipeline
|
expose :allow_merge_on_skipped_pipeline
|
||||||
|
|
|
@ -39,6 +39,7 @@ module API
|
||||||
|
|
||||||
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
|
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
|
||||||
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
|
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
|
||||||
|
optional :warn_about_potentially_unwanted_characters, type: Boolean, desc: 'Warn about Potentially Unwanted Characters'
|
||||||
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
|
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
|
||||||
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
|
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
|
||||||
optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge'
|
optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge'
|
||||||
|
|
|
@ -10,6 +10,8 @@ module API
|
||||||
|
|
||||||
feature_category :importers
|
feature_category :importers
|
||||||
|
|
||||||
|
before { authenticate! unless route.settings[:skip_authentication] }
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
def import_params
|
def import_params
|
||||||
declared_params(include_missing: false)
|
declared_params(include_missing: false)
|
||||||
|
@ -110,6 +112,7 @@ module API
|
||||||
detail 'This feature was introduced in GitLab 10.6.'
|
detail 'This feature was introduced in GitLab 10.6.'
|
||||||
success Entities::ProjectImportStatus
|
success Entities::ProjectImportStatus
|
||||||
end
|
end
|
||||||
|
route_setting :skip_authentication, true
|
||||||
get ':id/import' do
|
get ':id/import' do
|
||||||
present user_project, with: Entities::ProjectImportStatus
|
present user_project, with: Entities::ProjectImportStatus
|
||||||
end
|
end
|
||||||
|
|
|
@ -423,7 +423,7 @@ module API
|
||||||
authorize_admin_project
|
authorize_admin_project
|
||||||
attrs = declared_params(include_missing: false)
|
attrs = declared_params(include_missing: false)
|
||||||
authorize! :rename_project, user_project if attrs[:name].present?
|
authorize! :rename_project, user_project if attrs[:name].present?
|
||||||
authorize! :change_visibility_level, user_project if attrs[:visibility].present?
|
authorize! :change_visibility_level, user_project if user_project.visibility_attribute_present?(attrs)
|
||||||
|
|
||||||
attrs = translate_params_for_compatibility(attrs)
|
attrs = translate_params_for_compatibility(attrs)
|
||||||
filter_attributes_using_license!(attrs)
|
filter_attributes_using_license!(attrs)
|
||||||
|
|
|
@ -11,6 +11,10 @@ module Gitlab
|
||||||
|
|
||||||
has_one :saml_provider
|
has_one :saml_provider
|
||||||
|
|
||||||
|
def root_saml_provider
|
||||||
|
root_ancestor.saml_provider
|
||||||
|
end
|
||||||
|
|
||||||
def self.declarative_policy_class
|
def self.declarative_policy_class
|
||||||
"GroupPolicy"
|
"GroupPolicy"
|
||||||
end
|
end
|
||||||
|
|
|
@ -171,9 +171,11 @@ excluded_attributes:
|
||||||
- :marked_for_deletion_by_user_id
|
- :marked_for_deletion_by_user_id
|
||||||
- :compliance_framework_setting
|
- :compliance_framework_setting
|
||||||
- :show_default_award_emojis
|
- :show_default_award_emojis
|
||||||
|
- :warn_about_potentially_unwanted_characters
|
||||||
- :services
|
- :services
|
||||||
- :exported_protected_branches
|
- :exported_protected_branches
|
||||||
- :repository_size_limit
|
- :repository_size_limit
|
||||||
|
- :external_webhook_token
|
||||||
namespaces:
|
namespaces:
|
||||||
- :runners_token
|
- :runners_token
|
||||||
- :runners_token_encrypted
|
- :runners_token_encrypted
|
||||||
|
@ -377,6 +379,8 @@ excluded_attributes:
|
||||||
system_note_metadata:
|
system_note_metadata:
|
||||||
- :description_version_id
|
- :description_version_id
|
||||||
- :note_id
|
- :note_id
|
||||||
|
pipeline_schedules:
|
||||||
|
- :active
|
||||||
methods:
|
methods:
|
||||||
notes:
|
notes:
|
||||||
- :type
|
- :type
|
||||||
|
|
|
@ -84,6 +84,7 @@ module Gitlab
|
||||||
when :'Ci::Pipeline' then setup_pipeline
|
when :'Ci::Pipeline' then setup_pipeline
|
||||||
when *BUILD_MODELS then setup_build
|
when *BUILD_MODELS then setup_build
|
||||||
when :issues then setup_issue
|
when :issues then setup_issue
|
||||||
|
when :'Ci::PipelineSchedule' then setup_pipeline_schedule
|
||||||
end
|
end
|
||||||
|
|
||||||
update_project_references
|
update_project_references
|
||||||
|
@ -143,6 +144,10 @@ module Gitlab
|
||||||
@relation_hash['relative_position'] = compute_relative_position
|
@relation_hash['relative_position'] = compute_relative_position
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setup_pipeline_schedule
|
||||||
|
@relation_hash['active'] = false
|
||||||
|
end
|
||||||
|
|
||||||
def compute_relative_position
|
def compute_relative_position
|
||||||
return unless max_relative_position
|
return unless max_relative_position
|
||||||
|
|
||||||
|
|
19
lib/gitlab/unicode.rb
Normal file
19
lib/gitlab/unicode.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
class Unicode
|
||||||
|
# Regular expression for identifying bidirectional control
|
||||||
|
# characters in UTF-8 strings
|
||||||
|
#
|
||||||
|
# Documentation on how this works:
|
||||||
|
# https://idiosyncratic-ruby.com/41-proper-unicoding.html
|
||||||
|
BIDI_REGEXP = /\p{Bidi Control}/.freeze
|
||||||
|
|
||||||
|
class << self
|
||||||
|
# Warning message used to highlight bidi characters in the GUI
|
||||||
|
def bidi_warning
|
||||||
|
_("Potentially unwanted character detected: Unicode BiDi Control")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -155,6 +155,14 @@ module Gitlab
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visibility_attribute_value(attributes)
|
||||||
|
visibility_level_attributes.each do |attr|
|
||||||
|
return attributes[attr] if attributes.has_key?(attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def visibility_level_attributes
|
def visibility_level_attributes
|
||||||
[visibility_level_field, visibility_level_field.to_s,
|
[visibility_level_field, visibility_level_field.to_s,
|
||||||
:visibility, 'visibility']
|
:visibility, 'visibility']
|
||||||
|
|
|
@ -21,12 +21,24 @@ module Rouge
|
||||||
is_first = false
|
is_first = false
|
||||||
|
|
||||||
yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
|
yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
|
||||||
line.each { |token, value| yield span(token, value.chomp! || value) }
|
|
||||||
|
line.each do |token, value|
|
||||||
|
yield highlight_unicode_control_characters(span(token, value.chomp! || value))
|
||||||
|
end
|
||||||
|
|
||||||
yield %(</span>)
|
yield %(</span>)
|
||||||
|
|
||||||
@line_number += 1
|
@line_number += 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def highlight_unicode_control_characters(text)
|
||||||
|
text.gsub(Gitlab::Unicode::BIDI_REGEXP) do |char|
|
||||||
|
%(<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{Gitlab::Unicode.bidi_warning}">#{char}</span>)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25164,6 +25164,9 @@ msgstr ""
|
||||||
msgid "Postman collection file path or URL"
|
msgid "Postman collection file path or URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Potentially unwanted character detected: Unicode BiDi Control"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pre-defined push rules."
|
msgid "Pre-defined push rules."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -26253,6 +26256,9 @@ msgstr ""
|
||||||
msgid "ProjectSettings|Global"
|
msgid "ProjectSettings|Global"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectSettings|Highlight the usage of hidden unicode characters. These have innocent uses for right-to-left languages, but can also be used in potential exploits."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectSettings|Internal"
|
msgid "ProjectSettings|Internal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -26442,6 +26448,9 @@ msgstr ""
|
||||||
msgid "ProjectSettings|Visualize the project's performance metrics."
|
msgid "ProjectSettings|Visualize the project's performance metrics."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ProjectSettings|Warn about Potentially Unwanted Characters"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ProjectSettings|What are badges?"
|
msgid "ProjectSettings|What are badges?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -38764,6 +38773,9 @@ msgstr ""
|
||||||
msgid "[No reason]"
|
msgid "[No reason]"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "[REDACTED]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "[Redacted]"
|
msgid "[Redacted]"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -312,6 +312,34 @@ RSpec.describe ProjectsController do
|
||||||
|
|
||||||
expect { get_show }.not_to change { Gitlab::GitalyClient.get_request_count }
|
expect { get_show }.not_to change { Gitlab::GitalyClient.get_request_count }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "PUC highlighting" do
|
||||||
|
render_views
|
||||||
|
|
||||||
|
before do
|
||||||
|
expect(controller).to receive(:find_routable!).and_return(public_project)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "option is enabled" do
|
||||||
|
it "adds the highlighting class" do
|
||||||
|
expect(public_project).to receive(:warn_about_potentially_unwanted_characters?).and_return(true)
|
||||||
|
|
||||||
|
get_show
|
||||||
|
|
||||||
|
expect(response.body).to have_css(".project-highlight-puc")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "option is disabled" do
|
||||||
|
it "doesn't add the highlighting class" do
|
||||||
|
expect(public_project).to receive(:warn_about_potentially_unwanted_characters?).and_return(false)
|
||||||
|
|
||||||
|
get_show
|
||||||
|
|
||||||
|
expect(response.body).not_to have_css(".project-highlight-puc")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the url contains .atom" do
|
context "when the url contains .atom" do
|
||||||
|
|
|
@ -19,6 +19,10 @@ FactoryBot.define do
|
||||||
admin { true }
|
admin { true }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :public_email do
|
||||||
|
public_email { email }
|
||||||
|
end
|
||||||
|
|
||||||
trait :blocked do
|
trait :blocked do
|
||||||
after(:build) { |user, _| user.block! }
|
after(:build) { |user, _| user.block! }
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,12 +22,16 @@ const safeUrls = {
|
||||||
const unsafeUrls = [
|
const unsafeUrls = [
|
||||||
'/an/evil/url',
|
'/an/evil/url',
|
||||||
'../../../evil/url',
|
'../../../evil/url',
|
||||||
'https://evil.url/assets/icons-123a.svg',
|
'https://evil.url/assets/icons-123a.svg#test',
|
||||||
'https://evil.url/assets/icons-456b.svg',
|
'https://evil.url/assets/icons-456b.svg',
|
||||||
`https://evil.url/${rootGon.sprite_icons}`,
|
`https://evil.url/${rootGon.sprite_icons}`,
|
||||||
`https://evil.url/${rootGon.sprite_file_icons}`,
|
`https://evil.url/${rootGon.sprite_file_icons}`,
|
||||||
`https://evil.url/${absoluteGon.sprite_icons}`,
|
`https://evil.url/${absoluteGon.sprite_icons}`,
|
||||||
`https://evil.url/${absoluteGon.sprite_file_icons}`,
|
`https://evil.url/${absoluteGon.sprite_file_icons}`,
|
||||||
|
`${rootGon.sprite_icons}/../evil/path`,
|
||||||
|
`${rootGon.sprite_file_icons}/../../evil/path`,
|
||||||
|
`${absoluteGon.sprite_icons}/../evil/path`,
|
||||||
|
`${absoluteGon.sprite_file_icons}/../../https://evil.url`,
|
||||||
];
|
];
|
||||||
|
|
||||||
const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
|
const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
|
||||||
|
|
|
@ -607,6 +607,27 @@ describe('URL utility', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getNormalizedURL', () => {
|
||||||
|
it.each`
|
||||||
|
url | base | result
|
||||||
|
${'./foo'} | ${''} | ${'http://test.host/foo'}
|
||||||
|
${'../john.md'} | ${''} | ${'http://test.host/john.md'}
|
||||||
|
${'/images/img.png'} | ${'https://gitlab.com'} | ${'https://gitlab.com/images/img.png'}
|
||||||
|
${'/images/../img.png'} | ${'https://gitlab.com'} | ${'https://gitlab.com/img.png'}
|
||||||
|
${'/images/./img.png'} | ${'https://gitlab.com'} | ${'https://gitlab.com/images/img.png'}
|
||||||
|
${'./images/img.png'} | ${'https://gitlab.com/user/project'} | ${'https://gitlab.com/user/images/img.png'}
|
||||||
|
${'../images/../img.png'} | ${'https://gitlab.com/user/project'} | ${'https://gitlab.com/img.png'}
|
||||||
|
${'/images/img.png'} | ${'https://gitlab.com/user/project'} | ${'https://gitlab.com/images/img.png'}
|
||||||
|
${'/images/../img.png'} | ${'https://gitlab.com/user/project'} | ${'https://gitlab.com/img.png'}
|
||||||
|
${'/images/./img.png'} | ${'https://gitlab.com/user/project'} | ${'https://gitlab.com/images/img.png'}
|
||||||
|
`(
|
||||||
|
'converts url "$url" with base "$base" to normalized url => "expected"',
|
||||||
|
({ url, base, result }) => {
|
||||||
|
expect(urlUtils.getNormalizedURL(url, base)).toBe(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getWebSocketProtocol', () => {
|
describe('getWebSocketProtocol', () => {
|
||||||
it.each`
|
it.each`
|
||||||
protocol | expectation
|
protocol | expectation
|
||||||
|
|
|
@ -27,6 +27,7 @@ const defaultProps = {
|
||||||
emailsDisabled: false,
|
emailsDisabled: false,
|
||||||
packagesEnabled: true,
|
packagesEnabled: true,
|
||||||
showDefaultAwardEmojis: true,
|
showDefaultAwardEmojis: true,
|
||||||
|
warnAboutPotentiallyUnwantedCharacters: true,
|
||||||
},
|
},
|
||||||
isGitlabCom: true,
|
isGitlabCom: true,
|
||||||
canDisableEmails: true,
|
canDisableEmails: true,
|
||||||
|
@ -97,6 +98,10 @@ describe('Settings Panel', () => {
|
||||||
const findEmailSettings = () => wrapper.find({ ref: 'email-settings' });
|
const findEmailSettings = () => wrapper.find({ ref: 'email-settings' });
|
||||||
const findShowDefaultAwardEmojis = () =>
|
const findShowDefaultAwardEmojis = () =>
|
||||||
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
|
wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
|
||||||
|
const findWarnAboutPuc = () =>
|
||||||
|
wrapper.find(
|
||||||
|
'input[name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"]',
|
||||||
|
);
|
||||||
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
|
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
|
||||||
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
|
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
|
||||||
|
|
||||||
|
@ -539,6 +544,14 @@ describe('Settings Panel', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Warn about potentially unwanted characters', () => {
|
||||||
|
it('should have a "Warn about Potentially Unwanted Characters" input', () => {
|
||||||
|
wrapper = mountComponent();
|
||||||
|
|
||||||
|
expect(findWarnAboutPuc().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Metrics dashboard', () => {
|
describe('Metrics dashboard', () => {
|
||||||
it('should show the metrics dashboard access toggle', () => {
|
it('should show the metrics dashboard access toggle', () => {
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
|
||||||
described_class.new(object: nil, context: { current_user: user }, field: nil)
|
described_class.new(object: nil, context: { current_user: user }, field: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let_it_be(:author) { create(:user) }
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
describe '#resolve' do
|
describe '#resolve' do
|
||||||
|
@ -136,20 +137,37 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when user is the author and discussion is locked' do
|
||||||
|
let(:user) { author }
|
||||||
|
|
||||||
|
before do
|
||||||
|
issuable.update!(discussion_locked: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { mutation.resolve(id: id_arg, resolve: resolve_arg) }.to raise_error(
|
||||||
|
Gitlab::Graphql::Errors::ResourceNotAvailable,
|
||||||
|
"The resource that you are attempting to access does not exist or you don't have permission to perform this action"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when discussion is on a merge request' do
|
context 'when discussion is on a merge request' do
|
||||||
let_it_be(:noteable) { create(:merge_request, source_project: project) }
|
let_it_be(:noteable) { create(:merge_request, source_project: project, author: author) }
|
||||||
|
|
||||||
let(:discussion) { create(:diff_note_on_merge_request, noteable: noteable, project: project).to_discussion }
|
let(:discussion) { create(:diff_note_on_merge_request, noteable: noteable, project: project).to_discussion }
|
||||||
|
let(:issuable) { noteable }
|
||||||
|
|
||||||
it_behaves_like 'a working resolve method'
|
it_behaves_like 'a working resolve method'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when discussion is on a design' do
|
context 'when discussion is on a design' do
|
||||||
let_it_be(:noteable) { create(:design, :with_file, issue: create(:issue, project: project)) }
|
let_it_be(:noteable) { create(:design, :with_file, issue: create(:issue, project: project, author: author)) }
|
||||||
|
|
||||||
let(:discussion) { create(:diff_note_on_design, noteable: noteable, project: project).to_discussion }
|
let(:discussion) { create(:diff_note_on_design, noteable: noteable, project: project).to_discussion }
|
||||||
|
let(:issuable) { noteable.issue }
|
||||||
|
|
||||||
it_behaves_like 'a working resolve method'
|
it_behaves_like 'a working resolve method'
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,26 +3,58 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Mutations::Issues::SetSeverity do
|
RSpec.describe Mutations::Issues::SetSeverity do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:project) { create(:project) }
|
||||||
let_it_be(:issue) { create(:incident) }
|
let_it_be(:guest) { create(:user) }
|
||||||
|
let_it_be(:reporter) { create(:user) }
|
||||||
|
let_it_be(:issue) { create(:incident, project: project) }
|
||||||
|
|
||||||
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||||
|
|
||||||
specify { expect(described_class).to require_graphql_authorizations(:update_issue) }
|
specify { expect(described_class).to require_graphql_authorizations(:update_issue, :admin_issue) }
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
project.add_guest(guest)
|
||||||
|
project.add_reporter(reporter)
|
||||||
|
end
|
||||||
|
|
||||||
describe '#resolve' do
|
describe '#resolve' do
|
||||||
let(:severity) { 'critical' }
|
let(:severity) { 'critical' }
|
||||||
let(:mutated_incident) { subject[:issue] }
|
|
||||||
|
|
||||||
subject(:resolve) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, severity: severity) }
|
subject(:resolve) do
|
||||||
|
mutation.resolve(
|
||||||
|
project_path: issue.project.full_path,
|
||||||
|
iid: issue.iid,
|
||||||
|
severity: severity
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'permission level for issue mutation is correctly verified'
|
context 'as guest' do
|
||||||
|
let(:user) { guest }
|
||||||
|
|
||||||
context 'when the user can update the issue' do
|
it 'raises an error' do
|
||||||
before do
|
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
issue.project.add_developer(user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'and also author' do
|
||||||
|
let!(:issue) { create(:incident, project: project, author: user) }
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and also assignee' do
|
||||||
|
let!(:issue) { create(:incident, project: project, assignee_ids: [user.id]) }
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as reporter' do
|
||||||
|
let(:user) { reporter }
|
||||||
|
|
||||||
context 'when issue type is incident' do
|
context 'when issue type is incident' do
|
||||||
context 'when severity has a correct value' do
|
context 'when severity has a correct value' do
|
||||||
it 'updates severity' do
|
it 'updates severity' do
|
||||||
|
@ -48,9 +80,9 @@ RSpec.describe Mutations::Issues::SetSeverity do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when issue type is not incident' do
|
context 'when issue type is not incident' do
|
||||||
let!(:issue) { create(:issue) }
|
let!(:issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
it 'does not updates the issue' do
|
it 'does not update the issue' do
|
||||||
expect { resolve }.not_to change { issue.updated_at }
|
expect { resolve }.not_to change { issue.updated_at }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -953,4 +953,26 @@ RSpec.describe ProjectsHelper do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#project_classes' do
|
||||||
|
subject { helper.project_classes(project) }
|
||||||
|
|
||||||
|
it { is_expected.to be_a(String) }
|
||||||
|
|
||||||
|
context 'PUC highlighting enabled' do
|
||||||
|
before do
|
||||||
|
project.warn_about_potentially_unwanted_characters = true
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to include('project-highlight-puc') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'PUC highlighting disabled' do
|
||||||
|
before do
|
||||||
|
project.warn_about_potentially_unwanted_characters = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.not_to include('project-highlight-puc') }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
39
spec/lib/api/entities/project_spec.rb
Normal file
39
spec/lib/api/entities/project_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ::API::Entities::Project do
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
let(:current_user) { create(:user) }
|
||||||
|
let(:options) { { current_user: current_user } }
|
||||||
|
|
||||||
|
let(:entity) do
|
||||||
|
::API::Entities::Project.new(project, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:json) { entity.as_json }
|
||||||
|
|
||||||
|
describe '.shared_with_groups' do
|
||||||
|
let(:group) { create(:group, :private) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.project_group_links.create!(group: group)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current user does not have access to the group' do
|
||||||
|
it 'is empty' do
|
||||||
|
expect(json[:shared_with_groups]).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the current user has access to the group' do
|
||||||
|
before do
|
||||||
|
group.add_guest(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'contains information about the shared group' do
|
||||||
|
expect(json[:shared_with_groups]).to contain_exactly(include(group_id: group.id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Gitlab::DataBuilder::Build do
|
RSpec.describe Gitlab::DataBuilder::Build do
|
||||||
let!(:tag_names) { %w(tag-1 tag-2) }
|
let!(:tag_names) { %w(tag-1 tag-2) }
|
||||||
let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
|
let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n)}) }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user, :public_email) }
|
||||||
let(:build) { create(:ci_build, :running, runner: runner, user: user) }
|
let(:build) { create(:ci_build, :running, runner: runner, user: user) }
|
||||||
|
|
||||||
describe '.build' do
|
describe '.build' do
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::DataBuilder::Pipeline do
|
RSpec.describe Gitlab::DataBuilder::Pipeline do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user, :public_email) }
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
let_it_be_with_reload(:pipeline) do
|
let_it_be_with_reload(:pipeline) do
|
||||||
|
@ -46,7 +46,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
avatar_url: user.avatar_url(only_path: false),
|
avatar_url: user.avatar_url(only_path: false),
|
||||||
email: user.email
|
email: user.public_email
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -267,6 +267,35 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'pipeline_schedule' do
|
||||||
|
let(:relation_sym) { :pipeline_schedules }
|
||||||
|
let(:relation_hash) do
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"created_at": "2016-07-22T08:55:44.161Z",
|
||||||
|
"updated_at": "2016-07-22T08:55:44.161Z",
|
||||||
|
"description": "pipeline schedule",
|
||||||
|
"ref": "main",
|
||||||
|
"cron": "0 4 * * 0",
|
||||||
|
"cron_timezone": "UTC",
|
||||||
|
"active": value,
|
||||||
|
"project_id": project.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { created_object.active }
|
||||||
|
|
||||||
|
[true, false].each do |v|
|
||||||
|
context "when relation_hash has active set to #{v}" do
|
||||||
|
let(:value) { v }
|
||||||
|
|
||||||
|
it "the created object is not active" do
|
||||||
|
expect(created_object.active).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
|
# `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
|
||||||
# re-assigned by described_class.
|
# re-assigned by described_class.
|
||||||
context 'Potentially hazardous foreign keys' do
|
context 'Potentially hazardous foreign keys' do
|
||||||
|
|
|
@ -376,7 +376,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
|
||||||
expect(pipeline_schedule.ref).to eq('master')
|
expect(pipeline_schedule.ref).to eq('master')
|
||||||
expect(pipeline_schedule.cron).to eq('0 4 * * 0')
|
expect(pipeline_schedule.cron).to eq('0 4 * * 0')
|
||||||
expect(pipeline_schedule.cron_timezone).to eq('UTC')
|
expect(pipeline_schedule.cron_timezone).to eq('UTC')
|
||||||
expect(pipeline_schedule.active).to eq(true)
|
expect(pipeline_schedule.active).to eq(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -555,7 +555,6 @@ Project:
|
||||||
- merge_requests_rebase_enabled
|
- merge_requests_rebase_enabled
|
||||||
- jobs_cache_index
|
- jobs_cache_index
|
||||||
- external_authorization_classification_label
|
- external_authorization_classification_label
|
||||||
- external_webhook_token
|
|
||||||
- pages_https_only
|
- pages_https_only
|
||||||
- merge_requests_disable_committers_approval
|
- merge_requests_disable_committers_approval
|
||||||
- require_password_to_approve
|
- require_password_to_approve
|
||||||
|
|
33
spec/lib/gitlab/unicode_spec.rb
Normal file
33
spec/lib/gitlab/unicode_spec.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Unicode do
|
||||||
|
describe described_class::BIDI_REGEXP do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:bidi_string, :match) do
|
||||||
|
"\u2066" | true # left-to-right isolate
|
||||||
|
"\u2067" | true # right-to-left isolate
|
||||||
|
"\u2068" | true # first strong isolate
|
||||||
|
"\u2069" | true # pop directional isolate
|
||||||
|
"\u202a" | true # left-to-right embedding
|
||||||
|
"\u202b" | true # right-to-left embedding
|
||||||
|
"\u202c" | true # pop directional formatting
|
||||||
|
"\u202d" | true # left-to-right override
|
||||||
|
"\u202e" | true # right-to-left override
|
||||||
|
"\u2066foobar" | true
|
||||||
|
"" | false
|
||||||
|
"foo" | false
|
||||||
|
"\u2713" | false # checkmark
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
let(:utf8_string) { bidi_string.encode("utf-8") }
|
||||||
|
|
||||||
|
it "matches only the bidi characters" do
|
||||||
|
expect(utf8_string.match?(subject)).to eq(match)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,5 +36,26 @@ RSpec.describe Rouge::Formatters::HTMLGitlab do
|
||||||
is_expected.to eq(code)
|
is_expected.to eq(code)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when unicode control characters are used' do
|
||||||
|
let(:lang) { 'javascript' }
|
||||||
|
let(:tokens) { lexer.lex(code, continue: false) }
|
||||||
|
let(:code) do
|
||||||
|
<<~JS
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
var accessLevel = "user";
|
||||||
|
if (accessLevel != "user // Check if admin ") {
|
||||||
|
console.log("You are an admin.");
|
||||||
|
}
|
||||||
|
JS
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'highlights the control characters' do
|
||||||
|
message = "Potentially unwanted character detected: Unicode BiDi Control"
|
||||||
|
|
||||||
|
is_expected.to include(%{<span class="unicode-bidi has-tooltip" data-toggle="tooltip" title="#{message}">}).exactly(4).times
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
include StubRequests
|
include StubRequests
|
||||||
include Ci::SourcePipelineHelpers
|
include Ci::SourcePipelineHelpers
|
||||||
|
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user, :public_email) }
|
||||||
let_it_be(:namespace) { create_default(:namespace).freeze }
|
let_it_be(:namespace) { create_default(:namespace).freeze }
|
||||||
let_it_be(:project) { create_default(:project, :repository).freeze }
|
let_it_be(:project) { create_default(:project, :repository).freeze }
|
||||||
|
|
||||||
|
|
|
@ -188,6 +188,16 @@ RSpec.describe Discussion, ResolvableDiscussion do
|
||||||
it "returns true" do
|
it "returns true" do
|
||||||
expect(subject.can_resolve?(current_user)).to be true
|
expect(subject.can_resolve?(current_user)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when noteable is locked' do
|
||||||
|
before do
|
||||||
|
allow(subject.noteable).to receive(:discussion_locked?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(subject.can_resolve?(current_user)).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the signed in user can push to the project" do
|
context "when the signed in user can push to the project" do
|
||||||
|
@ -200,8 +210,11 @@ RSpec.describe Discussion, ResolvableDiscussion do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the noteable has no author" do
|
context "when the noteable has no author" do
|
||||||
|
before do
|
||||||
|
subject.noteable.author = nil
|
||||||
|
end
|
||||||
|
|
||||||
it "returns true" do
|
it "returns true" do
|
||||||
expect(noteable).to receive(:author).and_return(nil)
|
|
||||||
expect(subject.can_resolve?(current_user)).to be true
|
expect(subject.can_resolve?(current_user)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -213,8 +226,11 @@ RSpec.describe Discussion, ResolvableDiscussion do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the noteable has no author" do
|
context "when the noteable has no author" do
|
||||||
|
before do
|
||||||
|
subject.noteable.author = nil
|
||||||
|
end
|
||||||
|
|
||||||
it "returns false" do
|
it "returns false" do
|
||||||
expect(noteable).to receive(:author).and_return(nil)
|
|
||||||
expect(subject.can_resolve?(current_user)).to be false
|
expect(subject.can_resolve?(current_user)).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -660,6 +660,19 @@ RSpec.describe Project, factory_default: :keep do
|
||||||
it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
|
it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
|
||||||
it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
|
it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
|
||||||
|
|
||||||
|
describe 'project settings' do
|
||||||
|
%i(
|
||||||
|
show_default_award_emojis
|
||||||
|
show_default_award_emojis=
|
||||||
|
show_default_award_emojis?
|
||||||
|
warn_about_potentially_unwanted_characters
|
||||||
|
warn_about_potentially_unwanted_characters=
|
||||||
|
warn_about_potentially_unwanted_characters?
|
||||||
|
).each do |method|
|
||||||
|
it { is_expected.to delegate_method(method).to(:project_setting).with_arguments(allow_nil: true) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
include_examples 'ci_cd_settings delegation' do
|
include_examples 'ci_cd_settings delegation' do
|
||||||
# Skip attributes defined in EE code
|
# Skip attributes defined in EE code
|
||||||
let(:exclude_attributes) do
|
let(:exclude_attributes) do
|
||||||
|
|
|
@ -5511,16 +5511,29 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#hook_attrs' do
|
describe '#hook_attrs' do
|
||||||
it 'includes id, name, username, avatar_url, and email' do
|
let(:user) { create(:user) }
|
||||||
user = create(:user)
|
let(:user_attributes) do
|
||||||
user_attributes = {
|
{
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
avatar_url: user.avatar_url(only_path: false),
|
avatar_url: user.avatar_url(only_path: false)
|
||||||
email: user.email
|
|
||||||
}
|
}
|
||||||
expect(user.hook_attrs).to eq(user_attributes)
|
end
|
||||||
|
|
||||||
|
context 'with a public email' do
|
||||||
|
it 'includes id, name, username, avatar_url, and email' do
|
||||||
|
user.public_email = "hello@hello.com"
|
||||||
|
user_attributes[:email] = user.public_email
|
||||||
|
expect(user.hook_attrs).to eq(user_attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a public email' do
|
||||||
|
it "does not include email if user's email is private" do
|
||||||
|
user_attributes[:email] = "[REDACTED]"
|
||||||
|
expect(user.hook_attrs).to eq(user_attributes)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,10 @@ RSpec.describe IssuablePolicy, models: true do
|
||||||
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
|
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
|
||||||
let(:policies) { described_class.new(user, merge_request) }
|
let(:policies) { described_class.new(user, merge_request) }
|
||||||
|
|
||||||
|
it 'allows resolving notes' do
|
||||||
|
expect(policies).to be_allowed(:resolve_note)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when user is able to read project' do
|
context 'when user is able to read project' do
|
||||||
it 'enables user to read and update issuables' do
|
it 'enables user to read and update issuables' do
|
||||||
expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request)
|
expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request)
|
||||||
|
@ -43,16 +47,24 @@ RSpec.describe IssuablePolicy, models: true do
|
||||||
it 'can not create a note nor award emojis' do
|
it 'can not create a note nor award emojis' do
|
||||||
expect(policies).to be_disallowed(:create_note, :award_emoji)
|
expect(policies).to be_disallowed(:create_note, :award_emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not allow resolving note' do
|
||||||
|
expect(policies).to be_disallowed(:resolve_note)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is a project member' do
|
context 'when the user is a project member' do
|
||||||
before do
|
before do
|
||||||
project.add_guest(user)
|
project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can create a note and award emojis' do
|
it 'can create a note and award emojis' do
|
||||||
expect(policies).to be_allowed(:create_note, :award_emoji)
|
expect(policies).to be_allowed(:create_note, :award_emoji)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'allows resolving notes' do
|
||||||
|
expect(policies).to be_allowed(:resolve_note)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,13 +5,14 @@ require 'spec_helper'
|
||||||
RSpec.describe API::Applications, :api do
|
RSpec.describe API::Applications, :api do
|
||||||
let(:admin_user) { create(:user, admin: true) }
|
let(:admin_user) { create(:user, admin: true) }
|
||||||
let(:user) { create(:user, admin: false) }
|
let(:user) { create(:user, admin: false) }
|
||||||
let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: '') }
|
let(:scopes) { 'api' }
|
||||||
|
let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) }
|
||||||
|
|
||||||
describe 'POST /applications' do
|
describe 'POST /applications' do
|
||||||
context 'authenticated and authorized user' do
|
context 'authenticated and authorized user' do
|
||||||
it 'creates and returns an OAuth application' do
|
it 'creates and returns an OAuth application' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '' }
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
|
||||||
end.to change { Doorkeeper::Application.count }.by 1
|
end.to change { Doorkeeper::Application.count }.by 1
|
||||||
|
|
||||||
application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url')
|
application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url')
|
||||||
|
@ -22,11 +23,12 @@ RSpec.describe API::Applications, :api do
|
||||||
expect(json_response['secret']).to eq application.secret
|
expect(json_response['secret']).to eq application.secret
|
||||||
expect(json_response['callback_url']).to eq application.redirect_uri
|
expect(json_response['callback_url']).to eq application.redirect_uri
|
||||||
expect(json_response['confidential']).to eq application.confidential
|
expect(json_response['confidential']).to eq application.confidential
|
||||||
|
expect(application.scopes.to_s).to eq('api')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not allow creating an application with the wrong redirect_uri format' do
|
it 'does not allow creating an application with the wrong redirect_uri format' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://', scopes: '' }
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://', scopes: scopes }
|
||||||
end.not_to change { Doorkeeper::Application.count }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
@ -36,7 +38,7 @@ RSpec.describe API::Applications, :api do
|
||||||
|
|
||||||
it 'does not allow creating an application with a forbidden URI format' do
|
it 'does not allow creating an application with a forbidden URI format' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'javascript://alert()', scopes: '' }
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'javascript://alert()', scopes: scopes }
|
||||||
end.not_to change { Doorkeeper::Application.count }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
@ -46,7 +48,7 @@ RSpec.describe API::Applications, :api do
|
||||||
|
|
||||||
it 'does not allow creating an application without a name' do
|
it 'does not allow creating an application without a name' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { redirect_uri: 'http://application.url', scopes: '' }
|
post api('/applications', admin_user), params: { redirect_uri: 'http://application.url', scopes: scopes }
|
||||||
end.not_to change { Doorkeeper::Application.count }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
@ -56,7 +58,7 @@ RSpec.describe API::Applications, :api do
|
||||||
|
|
||||||
it 'does not allow creating an application without a redirect_uri' do
|
it 'does not allow creating an application without a redirect_uri' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { name: 'application_name', scopes: '' }
|
post api('/applications', admin_user), params: { name: 'application_name', scopes: scopes }
|
||||||
end.not_to change { Doorkeeper::Application.count }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
@ -64,19 +66,59 @@ RSpec.describe API::Applications, :api do
|
||||||
expect(json_response['error']).to eq('redirect_uri is missing')
|
expect(json_response['error']).to eq('redirect_uri is missing')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not allow creating an application without scopes' do
|
it 'does not allow creating an application without specifying `scopes`' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url' }
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url' }
|
||||||
end.not_to change { Doorkeeper::Application.count }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response).to be_a Hash
|
expect(json_response).to be_a Hash
|
||||||
expect(json_response['error']).to eq('scopes is missing')
|
expect(json_response['error']).to eq('scopes is missing, scopes is empty')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow creating an application with blank `scopes`' do
|
||||||
|
expect do
|
||||||
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '' }
|
||||||
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['error']).to eq('scopes is empty')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow creating an application with invalid `scopes`' do
|
||||||
|
expect do
|
||||||
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'non_existent_scope' }
|
||||||
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['message']['scopes'][0]).to eq('doesn\'t match configured on the server.')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'multiple scopes' do
|
||||||
|
it 'creates an application with multiple `scopes` when each scope specified is seperated by a space' do
|
||||||
|
expect do
|
||||||
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api read_user' }
|
||||||
|
end.to change { Doorkeeper::Application.count }.by 1
|
||||||
|
|
||||||
|
application = Doorkeeper::Application.last
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
|
expect(application.scopes.to_s).to eq('api read_user')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow creating an application with multiple `scopes` when one of the scopes is invalid' do
|
||||||
|
expect do
|
||||||
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api non_existent_scope' }
|
||||||
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['message']['scopes'][0]).to eq('doesn\'t match configured on the server.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'defaults to creating an application with confidential' do
|
it 'defaults to creating an application with confidential' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '', confidential: nil }
|
post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes, confidential: nil }
|
||||||
end.to change { Doorkeeper::Application.count }.by(1)
|
end.to change { Doorkeeper::Application.count }.by(1)
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
|
@ -89,7 +131,7 @@ RSpec.describe API::Applications, :api do
|
||||||
context 'authorized user without authorization' do
|
context 'authorized user without authorization' do
|
||||||
it 'does not create application' do
|
it 'does not create application' do
|
||||||
expect do
|
expect do
|
||||||
post api('/applications', user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '' }
|
post api('/applications', user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
|
||||||
end.not_to change { Doorkeeper::Application.count }
|
end.not_to change { Doorkeeper::Application.count }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:forbidden)
|
expect(response).to have_gitlab_http_status(:forbidden)
|
||||||
|
|
|
@ -758,6 +758,15 @@ RSpec.describe API::Groups do
|
||||||
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
|
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not update visibility_level if it is restricted' do
|
||||||
|
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||||
|
|
||||||
|
put api("/groups/#{group1.id}", user1), params: { visibility: 'internal' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['message']['visibility_level']).to include('internal has been restricted by your GitLab administrator')
|
||||||
|
end
|
||||||
|
|
||||||
context 'updating the `default_branch_protection` attribute' do
|
context 'updating the `default_branch_protection` attribute' do
|
||||||
subject do
|
subject do
|
||||||
put api("/groups/#{group1.id}", user1), params: { default_branch_protection: ::Gitlab::Access::PROTECTION_NONE }
|
put api("/groups/#{group1.id}", user1), params: { default_branch_protection: ::Gitlab::Access::PROTECTION_NONE }
|
||||||
|
@ -845,6 +854,15 @@ RSpec.describe API::Groups do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['name']).to eq(new_group_name)
|
expect(json_response['name']).to eq(new_group_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'ignores visibility level restrictions' do
|
||||||
|
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||||
|
|
||||||
|
put api("/groups/#{group1.id}", admin), params: { visibility: 'internal' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['visibility']).to eq('internal')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when authenticated as an user that can see the group' do
|
context 'when authenticated as an user that can see the group' do
|
||||||
|
|
|
@ -137,6 +137,7 @@ project_setting:
|
||||||
- has_confluence
|
- has_confluence
|
||||||
- has_vulnerabilities
|
- has_vulnerabilities
|
||||||
- prevent_merge_without_jira_issue
|
- prevent_merge_without_jira_issue
|
||||||
|
- warn_about_potentially_unwanted_characters
|
||||||
- previous_default_branch
|
- previous_default_branch
|
||||||
- project_id
|
- project_id
|
||||||
- push_rule_id
|
- push_rule_id
|
||||||
|
|
|
@ -16,6 +16,16 @@ RSpec.describe API::ProjectImport do
|
||||||
namespace.add_owner(user)
|
namespace.add_owner(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'requires authentication' do
|
||||||
|
let(:user) { nil }
|
||||||
|
|
||||||
|
it 'returns 401' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'POST /projects/import' do
|
describe 'POST /projects/import' do
|
||||||
subject { upload_archive(file_upload, workhorse_headers, params) }
|
subject { upload_archive(file_upload, workhorse_headers, params) }
|
||||||
|
|
||||||
|
@ -32,6 +42,8 @@ RSpec.describe API::ProjectImport do
|
||||||
allow(ImportExportUploader).to receive(:workhorse_upload_path).and_return('/')
|
allow(ImportExportUploader).to receive(:workhorse_upload_path).and_return('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'requires authentication'
|
||||||
|
|
||||||
it 'executes a limited number of queries' do
|
it 'executes a limited number of queries' do
|
||||||
control_count = ActiveRecord::QueryRecorder.new { subject }.count
|
control_count = ActiveRecord::QueryRecorder.new { subject }.count
|
||||||
|
|
||||||
|
@ -281,6 +293,10 @@ RSpec.describe API::ProjectImport do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /projects/remote-import' do
|
describe 'POST /projects/remote-import' do
|
||||||
|
subject do
|
||||||
|
post api('/projects/remote-import', user), params: params
|
||||||
|
end
|
||||||
|
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
path: 'test-import',
|
path: 'test-import',
|
||||||
|
@ -288,10 +304,12 @@ RSpec.describe API::ProjectImport do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'requires authentication'
|
||||||
|
|
||||||
it 'returns NOT FOUND when the feature is disabled' do
|
it 'returns NOT FOUND when the feature is disabled' do
|
||||||
stub_feature_flags(import_project_from_remote_file: false)
|
stub_feature_flags(import_project_from_remote_file: false)
|
||||||
|
|
||||||
post api('/projects/remote-import', user), params: params
|
subject
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
end
|
end
|
||||||
|
@ -315,7 +333,7 @@ RSpec.describe API::ProjectImport do
|
||||||
.to receive(:execute)
|
.to receive(:execute)
|
||||||
.and_return(service_response)
|
.and_return(service_response)
|
||||||
|
|
||||||
post api('/projects/remote-import', user), params: params
|
subject
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:created)
|
expect(response).to have_gitlab_http_status(:created)
|
||||||
expect(json_response).to include({
|
expect(json_response).to include({
|
||||||
|
@ -338,7 +356,7 @@ RSpec.describe API::ProjectImport do
|
||||||
.to receive(:execute)
|
.to receive(:execute)
|
||||||
.and_return(service_response)
|
.and_return(service_response)
|
||||||
|
|
||||||
post api('/projects/remote-import', user), params: params
|
subject
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
expect(json_response).to eq({
|
expect(json_response).to eq({
|
||||||
|
@ -350,6 +368,14 @@ RSpec.describe API::ProjectImport do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /projects/:id/import' do
|
describe 'GET /projects/:id/import' do
|
||||||
|
it 'public project accessible for an unauthenticated user' do
|
||||||
|
project = create(:project, :public)
|
||||||
|
|
||||||
|
get api("/projects/#{project.id}/import", nil)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns the import status' do
|
it 'returns the import status' do
|
||||||
project = create(:project, :import_started)
|
project = create(:project, :import_started)
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
|
@ -376,6 +402,8 @@ RSpec.describe API::ProjectImport do
|
||||||
describe 'POST /projects/import/authorize' do
|
describe 'POST /projects/import/authorize' do
|
||||||
subject { post api('/projects/import/authorize', user), headers: workhorse_headers }
|
subject { post api('/projects/import/authorize', user), headers: workhorse_headers }
|
||||||
|
|
||||||
|
it_behaves_like 'requires authentication'
|
||||||
|
|
||||||
it 'authorizes importing project with workhorse header' do
|
it 'authorizes importing project with workhorse header' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
|
|
|
@ -990,7 +990,7 @@ RSpec.describe API::Projects do
|
||||||
|
|
||||||
expect do
|
expect do
|
||||||
get api('/projects', admin)
|
get api('/projects', admin)
|
||||||
end.not_to exceed_query_limit(control.count)
|
end.not_to exceed_query_limit(control)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3203,6 +3203,15 @@ RSpec.describe API::Projects do
|
||||||
expect(json_response['visibility']).to eq('private')
|
expect(json_response['visibility']).to eq('private')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not update visibility_level if it is restricted' do
|
||||||
|
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||||
|
|
||||||
|
put api("/projects/#{project3.id}", user), params: { visibility: 'internal' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
expect(json_response['message']['visibility_level']).to include('internal has been restricted by your GitLab administrator')
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not update name to existing name' do
|
it 'does not update name to existing name' do
|
||||||
project_param = { name: project3.name }
|
project_param = { name: project3.name }
|
||||||
|
|
||||||
|
@ -3526,6 +3535,19 @@ RSpec.describe API::Projects do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when authenticated as the admin' do
|
||||||
|
let_it_be(:admin) { create(:admin) }
|
||||||
|
|
||||||
|
it 'ignores visibility level restrictions' do
|
||||||
|
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
|
||||||
|
|
||||||
|
put api("/projects/#{project3.id}", admin), params: { visibility: 'internal' }
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['visibility']).to eq('internal')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when updating repository storage' do
|
context 'when updating repository storage' do
|
||||||
let(:unknown_storage) { 'new-storage' }
|
let(:unknown_storage) { 'new-storage' }
|
||||||
let(:new_project) { create(:project, :repository, namespace: user.namespace) }
|
let(:new_project) { create(:project, :repository, namespace: user.namespace) }
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Groups::TransferService do
|
RSpec.describe Groups::TransferService, :sidekiq_inline do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:new_parent_group) { create(:group, :public) }
|
let_it_be(:new_parent_group) { create(:group, :public) }
|
||||||
|
|
||||||
|
@ -594,6 +594,59 @@ RSpec.describe Groups::TransferService do
|
||||||
transfer_service.execute(new_parent_group)
|
transfer_service.execute(new_parent_group)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'transferring groups with shared_projects' do
|
||||||
|
let_it_be_with_reload(:shared_project) { create(:project, :public) }
|
||||||
|
|
||||||
|
shared_examples_for 'drops the authorizations of ancestor members from the old hierarchy' do
|
||||||
|
it 'drops the authorizations of ancestor members from the old hierarchy' do
|
||||||
|
expect { transfer_service.execute(new_parent_group) }.to change {
|
||||||
|
ProjectAuthorization.where(project: shared_project, user: old_group_member).size
|
||||||
|
}.from(1).to(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the group that has existing project share is transferred' do
|
||||||
|
before do
|
||||||
|
create(:project_group_link, :maintainer, project: shared_project, group: group)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'drops the authorizations of ancestor members from the old hierarchy'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the group whose subgroup has an existing project share is transferred' do
|
||||||
|
let_it_be_with_reload(:subgroup) { create(:group, :private, parent: group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:project_group_link, :maintainer, project: shared_project, group: subgroup)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'drops the authorizations of ancestor members from the old hierarchy'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a group that has existing group share is transferred' do
|
||||||
|
let(:shared_with_group) { group }
|
||||||
|
|
||||||
|
let_it_be(:member_of_shared_with_group) { create(:user) }
|
||||||
|
let_it_be(:shared_group) { create(:group, :private) }
|
||||||
|
let_it_be(:project_in_shared_group) { create(:project, namespace: shared_group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
shared_with_group.add_developer(member_of_shared_with_group)
|
||||||
|
create(:group_group_link, :maintainer, shared_group: shared_group, shared_with_group: shared_with_group)
|
||||||
|
shared_with_group.refresh_members_authorized_projects
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'retains the authorizations of direct members' do
|
||||||
|
expect { transfer_service.execute(new_parent_group) }.not_to change {
|
||||||
|
ProjectAuthorization.where(
|
||||||
|
project: project_in_shared_group,
|
||||||
|
user: member_of_shared_with_group,
|
||||||
|
access_level: Gitlab::Access::DEVELOPER).size
|
||||||
|
}.from(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:user2) { create(:user) }
|
let_it_be(:user2) { create(:user) }
|
||||||
let_it_be(:user3) { create(:user) }
|
let_it_be(:user3) { create(:user) }
|
||||||
|
let_it_be(:guest) { create(:user) }
|
||||||
let_it_be(:group) { create(:group, :public) }
|
let_it_be(:group) { create(:group, :public) }
|
||||||
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
|
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
|
||||||
let_it_be(:label) { create(:label, project: project) }
|
let_it_be(:label) { create(:label, project: project) }
|
||||||
|
@ -24,6 +25,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
project.add_developer(user2)
|
project.add_developer(user2)
|
||||||
project.add_developer(user3)
|
project.add_developer(user3)
|
||||||
|
project.add_guest(guest)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'execute' do
|
describe 'execute' do
|
||||||
|
@ -95,9 +97,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'user is a guest' do
|
context 'user is a guest' do
|
||||||
before do
|
let(:user) { guest }
|
||||||
project.add_guest(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not assign the sentry error' do
|
it 'does not assign the sentry error' do
|
||||||
update_issue(opts)
|
update_issue(opts)
|
||||||
|
@ -245,11 +245,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
|
|
||||||
context 'from issue to restricted issue types' do
|
context 'from issue to restricted issue types' do
|
||||||
context 'without sufficient permissions' do
|
context 'without sufficient permissions' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { guest }
|
||||||
|
|
||||||
before do
|
|
||||||
project.add_guest(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does nothing to the labels' do
|
it 'does nothing to the labels' do
|
||||||
expect { update_issue(issue_type: 'issue') }.not_to change(issue.labels, :count)
|
expect { update_issue(issue_type: 'issue') }.not_to change(issue.labels, :count)
|
||||||
|
@ -394,12 +390,6 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when current user cannot admin issues in the project' do
|
context 'when current user cannot admin issues in the project' do
|
||||||
let(:guest) { create(:user) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
project.add_guest(guest)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'filters out params that cannot be set without the :admin_issue permission' do
|
it 'filters out params that cannot be set without the :admin_issue permission' do
|
||||||
described_class.new(
|
described_class.new(
|
||||||
project: project, current_user: guest, params: opts.merge(
|
project: project, current_user: guest, params: opts.merge(
|
||||||
|
@ -1100,6 +1090,24 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
|
|
||||||
it_behaves_like 'does not change the severity'
|
it_behaves_like 'does not change the severity'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'as guest' do
|
||||||
|
let(:user) { guest }
|
||||||
|
|
||||||
|
it_behaves_like 'does not change the severity'
|
||||||
|
|
||||||
|
context 'and also author' do
|
||||||
|
let(:issue) { create(:incident, project: project, author: user) }
|
||||||
|
|
||||||
|
it_behaves_like 'does not change the severity'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and also assignee' do
|
||||||
|
let(:issue) { create(:incident, project: project, assignee_ids: [user.id]) }
|
||||||
|
|
||||||
|
it_behaves_like 'does not change the severity'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when severity has been set before' do
|
context 'when severity has been set before' do
|
||||||
|
|
|
@ -270,7 +270,7 @@ func TestUploadHandlerForMultipleFiles(t *testing.T) {
|
||||||
require.NoError(t, s.writer.Close())
|
require.NoError(t, s.writer.Close())
|
||||||
|
|
||||||
response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
|
response := testUploadArtifacts(t, s.writer.FormDataContentType(), s.url, s.buffer)
|
||||||
require.Equal(t, http.StatusInternalServerError, response.Code)
|
require.Equal(t, http.StatusBadRequest, response.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUploadFormProcessing(t *testing.T) {
|
func TestUploadFormProcessing(t *testing.T) {
|
||||||
|
|
BIN
workhorse/internal/upload/exif/testdata/takes_lot_of_memory_to_decode.tiff
vendored
Normal file
BIN
workhorse/internal/upload/exif/testdata/takes_lot_of_memory_to_decode.tiff
vendored
Normal file
Binary file not shown.
|
@ -25,7 +25,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInjectedClientParam means that the client sent a parameter that overrides one of our own fields
|
// ErrInjectedClientParam means that the client sent a parameter that overrides one of our own fields
|
||||||
var ErrInjectedClientParam = errors.New("injected client parameter")
|
var (
|
||||||
|
ErrInjectedClientParam = errors.New("injected client parameter")
|
||||||
|
ErrMultipleFilesUploaded = errors.New("upload request contains more than one file")
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
multipartUploadRequests = promauto.NewCounterVec(
|
multipartUploadRequests = promauto.NewCounterVec(
|
||||||
|
@ -114,6 +117,10 @@ func rewriteFormFilesFromMultipart(r *http.Request, writer *multipart.Writer, pr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipart.Part, opts *filestore.SaveFileOpts) error {
|
func (rew *rewriter) handleFilePart(ctx context.Context, name string, p *multipart.Part, opts *filestore.SaveFileOpts) error {
|
||||||
|
if rew.filter.Count() > 0 {
|
||||||
|
return ErrMultipleFilesUploaded
|
||||||
|
}
|
||||||
|
|
||||||
multipartFiles.WithLabelValues(rew.filter.Name()).Inc()
|
multipartFiles.WithLabelValues(rew.filter.Name()).Inc()
|
||||||
|
|
||||||
filename := filepath.Base(p.FileName())
|
filename := filepath.Base(p.FileName())
|
||||||
|
@ -226,7 +233,7 @@ func handleExifUpload(ctx context.Context, r io.Reader, filename string, imageTy
|
||||||
}
|
}
|
||||||
|
|
||||||
func isTIFF(r io.Reader) bool {
|
func isTIFF(r io.Reader) bool {
|
||||||
_, err := tiff.Decode(r)
|
_, err := tiff.DecodeConfig(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package upload
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -29,6 +30,10 @@ func TestImageTypeRecongition(t *testing.T) {
|
||||||
filename: "exif/testdata/sample_exif_invalid.jpg",
|
filename: "exif/testdata/sample_exif_invalid.jpg",
|
||||||
isJPEG: false,
|
isJPEG: false,
|
||||||
isTIFF: false,
|
isTIFF: false,
|
||||||
|
}, {
|
||||||
|
filename: "exif/testdata/takes_lot_of_memory_to_decode.tiff", // File from https://gitlab.com/gitlab-org/gitlab/-/issues/341363
|
||||||
|
isJPEG: false,
|
||||||
|
isTIFF: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,8 +41,16 @@ func TestImageTypeRecongition(t *testing.T) {
|
||||||
t.Run(test.filename, func(t *testing.T) {
|
t.Run(test.filename, func(t *testing.T) {
|
||||||
input, err := os.Open(test.filename)
|
input, err := os.Open(test.filename)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
start := m.TotalAlloc
|
||||||
|
|
||||||
require.Equal(t, test.isJPEG, isJPEG(input))
|
require.Equal(t, test.isJPEG, isJPEG(input))
|
||||||
require.Equal(t, test.isTIFF, isTIFF(input))
|
require.Equal(t, test.isTIFF, isTIFF(input))
|
||||||
|
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
require.Less(t, m.TotalAlloc-start, uint64(50000), "must take reasonable amount of memory to recognise the type")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ type MultipartFormProcessor interface {
|
||||||
ProcessField(ctx context.Context, formName string, writer *multipart.Writer) error
|
ProcessField(ctx context.Context, formName string, writer *multipart.Writer) error
|
||||||
Finalize(ctx context.Context) error
|
Finalize(ctx context.Context) error
|
||||||
Name() string
|
Name() string
|
||||||
|
Count() int
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleFileUploads(w http.ResponseWriter, r *http.Request, h http.Handler, preauth *api.Response, filter MultipartFormProcessor, opts *filestore.SaveFileOpts) {
|
func HandleFileUploads(w http.ResponseWriter, r *http.Request, h http.Handler, preauth *api.Response, filter MultipartFormProcessor, opts *filestore.SaveFileOpts) {
|
||||||
|
@ -34,6 +35,8 @@ func HandleFileUploads(w http.ResponseWriter, r *http.Request, h http.Handler, p
|
||||||
switch err {
|
switch err {
|
||||||
case ErrInjectedClientParam:
|
case ErrInjectedClientParam:
|
||||||
helper.CaptureAndFail(w, r, err, "Bad Request", http.StatusBadRequest)
|
helper.CaptureAndFail(w, r, err, "Bad Request", http.StatusBadRequest)
|
||||||
|
case ErrMultipleFilesUploaded:
|
||||||
|
helper.CaptureAndFail(w, r, err, "Uploading multiple files is not allowed", http.StatusBadRequest)
|
||||||
case http.ErrNotMultipart:
|
case http.ErrNotMultipart:
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
case filestore.ErrEntityTooLarge:
|
case filestore.ErrEntityTooLarge:
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
||||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/filestore"
|
|
||||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
||||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/objectstore/test"
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/objectstore/test"
|
||||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/proxy"
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/proxy"
|
||||||
|
@ -28,11 +27,7 @@ import (
|
||||||
|
|
||||||
var nilHandler = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
var nilHandler = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||||
|
|
||||||
type testFormProcessor struct{}
|
type testFormProcessor struct{ SavedFileTracker }
|
||||||
|
|
||||||
func (a *testFormProcessor) ProcessFile(ctx context.Context, formName string, file *filestore.FileHandler, writer *multipart.Writer) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *testFormProcessor) ProcessField(ctx context.Context, formName string, writer *multipart.Writer) error {
|
func (a *testFormProcessor) ProcessField(ctx context.Context, formName string, writer *multipart.Writer) error {
|
||||||
if formName != "token" && !strings.HasPrefix(formName, "file.") && !strings.HasPrefix(formName, "other.") {
|
if formName != "token" && !strings.HasPrefix(formName, "file.") && !strings.HasPrefix(formName, "other.") {
|
||||||
|
@ -45,10 +40,6 @@ func (a *testFormProcessor) Finalize(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *testFormProcessor) Name() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUploadTempPathRequirement(t *testing.T) {
|
func TestUploadTempPathRequirement(t *testing.T) {
|
||||||
apiResponse := &api.Response{}
|
apiResponse := &api.Response{}
|
||||||
preparer := &DefaultPreparer{}
|
preparer := &DefaultPreparer{}
|
||||||
|
@ -259,6 +250,38 @@ func TestUploadProcessingField(t *testing.T) {
|
||||||
require.Equal(t, 500, response.Code)
|
require.Equal(t, 500, response.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUploadingMultipleFiles(t *testing.T) {
|
||||||
|
testhelper.ConfigureSecret()
|
||||||
|
|
||||||
|
tempPath, err := ioutil.TempDir("", "uploads")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempPath)
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
writer := multipart.NewWriter(&buffer)
|
||||||
|
_, err = writer.CreateFormFile("file", "my.file")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = writer.CreateFormFile("file", "my.file")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, writer.Close())
|
||||||
|
|
||||||
|
httpRequest, err := http.NewRequest("PUT", "/url/path", &buffer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
httpRequest.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
apiResponse := &api.Response{TempPath: tempPath}
|
||||||
|
preparer := &DefaultPreparer{}
|
||||||
|
opts, _, err := preparer.Prepare(apiResponse)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
HandleFileUploads(response, httpRequest, nilHandler, apiResponse, &testFormProcessor{}, opts)
|
||||||
|
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, "Uploading multiple files is not allowed\n", response.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestUploadProcessingFile(t *testing.T) {
|
func TestUploadProcessingFile(t *testing.T) {
|
||||||
tempPath, err := ioutil.TempDir("", "uploads")
|
tempPath, err := ioutil.TempDir("", "uploads")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
Loading…
Reference in a new issue