Commit 214b6b25 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-09-04

# Conflicts:
#	.gitlab-ci.yml
#	app/controllers/projects/application_controller.rb

[ci skip]
parents f5aed7cf 383dfd17
...@@ -201,7 +201,11 @@ stages: ...@@ -201,7 +201,11 @@ stages:
- '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}' - '[[ -f $FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_REPORT_PATH}'
- '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}' - '[[ -f $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
<<<<<<< HEAD
- knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml --tag ~geo" - knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml --tag ~geo"
=======
- knapsack rspec "--color --format documentation --format RspecJunitFormatter --out junit_rspec.xml"
>>>>>>> upstream/master
artifacts: artifacts:
expire_in: 31d expire_in: 31d
when: always when: always
......
...@@ -4,11 +4,6 @@ ...@@ -4,11 +4,6 @@
margin-top: 20px; margin-top: 20px;
} }
.container-fluid {
padding-left: 5px;
padding-right: 5px;
}
.nav-links > li > a { .nav-links > li > a {
padding: 10px; padding: 10px;
font-size: 12px; font-size: 12px;
......
module IssuableCollections module IssuableCollections
prepend EE::IssuableCollections prepend EE::IssuableCollections
extend ActiveSupport::Concern extend ActiveSupport::Concern
include CookiesHelper
include SortingHelper include SortingHelper
include Gitlab::IssuableMetadata include Gitlab::IssuableMetadata
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
...@@ -108,11 +109,14 @@ module IssuableCollections ...@@ -108,11 +109,14 @@ module IssuableCollections
end end
def set_sort_order_from_cookie def set_sort_order_from_cookie
cookies[remember_sorting_key] = params[:sort] if params[:sort].present? sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility # fallback to legacy cookie value for backward compatibility
cookies[remember_sorting_key] ||= cookies['issuable_sort'] sort_param ||= cookies['issuable_sort']
cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key]) sort_param ||= cookies[remember_sorting_key]
params[:sort] = cookies[remember_sorting_key]
sort_value = update_cookie_value(sort_param)
set_secure_cookie(remember_sorting_key, sort_value)
params[:sort] = sort_value
end end
def remember_sorting_key def remember_sorting_key
......
class Projects::ApplicationController < ApplicationController class Projects::ApplicationController < ApplicationController
<<<<<<< HEAD
prepend EE::Projects::ApplicationController prepend EE::Projects::ApplicationController
=======
include CookiesHelper
>>>>>>> upstream/master
include RoutableActions include RoutableActions
include ChecksCollaboration include ChecksCollaboration
...@@ -75,7 +79,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -75,7 +79,7 @@ class Projects::ApplicationController < ApplicationController
end end
def apply_diff_view_cookie! def apply_diff_view_cookie!
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present?
end end
def require_pages_enabled! def require_pages_enabled!
......
# frozen_string_literal: true
module CookiesHelper
def set_secure_cookie(key, value, httponly: false, permanent: false)
cookie_jar = permanent ? cookies.permanent : cookies
cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly }
end
end
module WikiHelper module WikiHelper
include API::Helpers::RelatedResourcesHelpers
# Produces a pure text breadcrumb for a given page. # Produces a pure text breadcrumb for a given page.
# #
# page_slug - The slug of a WikiPage object. # page_slug - The slug of a WikiPage object.
...@@ -39,4 +41,8 @@ module WikiHelper ...@@ -39,4 +41,8 @@ module WikiHelper
end end
end end
end end
def wiki_attachment_upload_url
expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
end
end end
...@@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy ...@@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy
rule { assignee_or_author }.policy do rule { assignee_or_author }.policy do
enable :read_issue enable :read_issue
enable :update_issue enable :update_issue
enable :reopen_issue
enable :read_merge_request enable :read_merge_request
enable :update_merge_request enable :update_merge_request
end end
......
...@@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy ...@@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy
prevent :update_issue prevent :update_issue
prevent :admin_issue prevent :admin_issue
end end
rule { locked }.policy do
prevent :reopen_issue
end
end end
...@@ -181,6 +181,7 @@ class ProjectPolicy < BasePolicy ...@@ -181,6 +181,7 @@ class ProjectPolicy < BasePolicy
enable :fork_project enable :fork_project
enable :create_project_snippet enable :create_project_snippet
enable :update_issue enable :update_issue
enable :reopen_issue
enable :admin_issue enable :admin_issue
enable :admin_label enable :admin_label
enable :admin_list enable :admin_list
......
...@@ -7,8 +7,8 @@ module Files ...@@ -7,8 +7,8 @@ module Files
def initialize(*args) def initialize(*args)
super super
@author_email = params[:author_email] @author_email = params[:author_email] || current_user&.email
@author_name = params[:author_name] @author_name = params[:author_name] || current_user&.name
@commit_message = params[:commit_message] @commit_message = params[:commit_message]
@last_commit_sha = params[:last_commit_sha] @last_commit_sha = params[:last_commit_sha]
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Issues module Issues
class ReopenService < Issues::BaseService class ReopenService < Issues::BaseService
def execute(issue) def execute(issue)
return issue unless can?(current_user, :update_issue, issue) return issue unless can?(current_user, :reopen_issue, issue)
if issue.reopen if issue.reopen
event_service.reopen_issue(issue, current_user) event_service.reopen_issue(issue, current_user)
......
# frozen_string_literal: true
module Wikis
class CreateAttachmentService < Files::CreateService
ATTACHMENT_PATH = 'uploads'.freeze
MAX_FILENAME_LENGTH = 255
delegate :wiki, to: :project
delegate :repository, to: :wiki
def initialize(*args)
super
@file_name = truncate_file_name(params[:file_name])
@file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
@commit_message ||= "Upload attachment #{@file_name}"
@branch_name ||= wiki.default_branch
end
def create_commit!
commit_result(create_transformed_commit(@file_content))
end
private
def truncate_file_name(file_name)
return unless file_name.present?
return file_name if file_name.length <= MAX_FILENAME_LENGTH
extension = File.extname(file_name)
truncate_at = MAX_FILENAME_LENGTH - extension.length - 1
base_name = File.basename(file_name, extension)[0..truncate_at]
base_name + extension
end
def validate!
validate_file_name!
validate_permissions!
end
def validate_file_name!
raise_error('The file name cannot be empty') unless @file_name
end
def validate_permissions!
unless can?(current_user, :create_wiki, project)
raise_error('You are not allowed to push to the wiki')
end
end
def create_transformed_commit(content)
repository.create_file(
current_user,
@file_path,
content,
message: @commit_message,
branch_name: @branch_name,
author_email: @author_email,
author_name: @author_name)
end
def commit_result(commit_id)
{
file_name: @file_name,
file_path: @file_path,
branch: @branch_name,
commit: commit_id
}
end
end
end
...@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader ...@@ -122,12 +122,6 @@ class FileUploader < GitlabUploader
} }
end end
def markdown_link
markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
markdown
end
def to_h def to_h
{ {
alt: markdown_name, alt: markdown_name,
...@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader ...@@ -192,10 +186,6 @@ class FileUploader < GitlabUploader
storage.delete_dir!(store_dir) # only remove when empty storage.delete_dir!(store_dir) # only remove when empty
end end
def markdown_name
(image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
end
def identifier def identifier
@identifier ||= filename @identifier ||= filename
end end
......
...@@ -2,32 +2,7 @@ ...@@ -2,32 +2,7 @@
# Extra methods for uploader # Extra methods for uploader
module UploaderHelper module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze include Gitlab::FileMarkdownLinkBuilder
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
# These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
DANGEROUS_EXT = %w[svg].freeze
def image?
extension_match?(IMAGE_EXT)
end
def video?
extension_match?(VIDEO_EXT)
end
def image_or_video?
image? || video?
end
def dangerous?
extension_match?(DANGEROUS_EXT)
end
private private
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
- page_card_attributes @issue.card_attributes - page_card_attributes @issue.card_attributes
- can_update_issue = can?(current_user, :update_issue, @issue) - can_update_issue = can?(current_user, :update_issue, @issue)
- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project) - can_create_issue = show_new_issue_link?(@project)
...@@ -40,6 +41,7 @@ ...@@ -40,6 +41,7 @@
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue - if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if can_reopen_issue
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam - if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
...@@ -48,7 +50,7 @@ ...@@ -48,7 +50,7 @@
%li.divider %li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue
- if can_report_spam - if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
......
...@@ -39,4 +39,4 @@ ...@@ -39,4 +39,4 @@
- if can_update_merge_request - if can_update_merge_request
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit" = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit"
= render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request
...@@ -41,3 +41,8 @@ ...@@ -41,3 +41,8 @@
= render 'sidebar' = render 'sidebar'
#delete-wiki-modal.modal.fade #delete-wiki-modal.modal.fade
- content_for :scripts_body do
-# haml-lint:disable InlineJavaScript
:javascript
window.uploads_path = "#{wiki_attachment_upload_url}";
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
- display_issuable_type = issuable_display_type(issuable) - display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable) - button_method = issuable_close_reopen_button_method(issuable)
- if can_update && is_current_user - if can_update
= link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, - if is_current_user
class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - else
- elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable - if can_reopen && is_current_user
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- else - else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
---
title: Restrict reopening locked issues for non authorized issue authors
merge_request: 21299
author:
type: changed
---
title: Remove unused CSS part in mobile framework
merge_request: 21439
author: Takuya Noguchi
type: other
---
title: Fix edge cases of JUnitParser
merge_request: 21469
author:
type: fixed
---
title: Store wiki uploads inside git repository
merge_request: 21362
author:
type: added
---
title: 'Rails 5: support schema t.index for mysql'
merge_request: 21485
author: Jasper Maes
type: other
---
title: Send back required object storage PUT headers in /uploads/authorize API
merge_request: 21319
author:
type: changed
---
title: Set issuable_sort, diff_view, and perf_bar_enabled cookies to secure when possible
merge_request: 21442
author:
type: security
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
# using the MySQL adapter apply a length of 20. Otherwise MySQL can't create an # using the MySQL adapter apply a length of 20. Otherwise MySQL can't create an
# index on binary columns. # index on binary columns.
# This module can be removed once a Rails 5 schema is used.
# It can't be wrapped in a check that checks Gitlab.rails5? because
# the old Rails 4 schema layout is still used
module MysqlSetLengthForBinaryIndex module MysqlSetLengthForBinaryIndex
def add_index(table_name, column_names, options = {}) def add_index(table_name, column_names, options = {})
Array(column_names).each do |column_name| Array(column_names).each do |column_name|
...@@ -19,3 +22,28 @@ end ...@@ -19,3 +22,28 @@ end
if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:prepend, MysqlSetLengthForBinaryIndex) ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:prepend, MysqlSetLengthForBinaryIndex)
end end
if Gitlab.rails5?
module MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema
# This method is used in Rails 5 schema loading as t.index
def index(column_names, options = {})
Array(column_names).each do |column_name|
column = columns.find { |c| c.name == column_name }
if column&.type == :binary
options[:length] = 20
end
end
# Ignore indexes that use opclasses,
# also see config/initializers/mysql_ignore_postgresql_options.rb
unless options[:opclasses]
super(column_names, options)
end
end
end
if defined?(ActiveRecord::ConnectionAdapters::MySQL::TableDefinition)
ActiveRecord::ConnectionAdapters::MySQL::TableDefinition.send(:prepend, MysqlSetLengthForBinaryIndexAndIgnorePostgresOptionsForSchema)
end
end
...@@ -21,5 +21,5 @@ end ...@@ -21,5 +21,5 @@ end
has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('Pick into') } has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('Pick into') }
if gitlab.branch_for_base != "master" && !has_pick_into_stable_label if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
warn "Most of the time, all merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label." warn "Most of the time, merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
end end
...@@ -12,8 +12,8 @@ In the following table you can see the phases a trace goes through. ...@@ -12,8 +12,8 @@ In the following table you can see the phases a trace goes through.
| ----- | ----- | --------- | --------- | ----------- | | ----- | ----- | --------- | --------- | ----------- |
| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| | 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`| | 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`| | 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
| 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`| | 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
The `ROOT_PATH` varies per your environment. For Omnibus GitLab it The `ROOT_PATH` varies per your environment. For Omnibus GitLab it
would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source
...@@ -88,6 +88,8 @@ To archive those legacy job traces, please follow the instruction below. ...@@ -88,6 +88,8 @@ To archive those legacy job traces, please follow the instruction below.
## How to migrate archived job traces to object storage ## How to migrate archived job traces to object storage
> [Introduced][ce-21193] in GitLab 11.3.
If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below. If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below.
1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled 1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled
...@@ -201,4 +203,5 @@ indicate that we have trace chunk. `UPDATE`s with 128KB of data is issued once w ...@@ -201,4 +203,5 @@ indicate that we have trace chunk. `UPDATE`s with 128KB of data is issued once w
receive multiple chunks. receive multiple chunks.
[ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169 [ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
[ce-21193]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21193
[ce-46097]: https://gitlab.com/gitlab-org/gitlab-ce/issues/46097 [ce-46097]: https://gitlab.com/gitlab-org/gitlab-ce/issues/46097
...@@ -97,12 +97,12 @@ curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKE ...@@ -97,12 +97,12 @@ curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKE
Example response: Example response:
```json ```json
{ {
"content" : "Hello world", "content" : "Hello world",
"format" : "markdown", "format" : "markdown",
"slug" : "Hello", "slug" : "Hello",
"title" : "Hello" "title" : "Hello"
} }
``` ```
## Edit an existing wiki page ## Edit an existing wiki page
...@@ -154,6 +154,44 @@ DELETE /projects/:id/wikis/:slug ...@@ -154,6 +154,44 @@ DELETE /projects/:id/wikis/:slug
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo"
``` ```
On success the HTTP status code is `204` and no JSON response is expected. On success the HTTP status code is `204` and no JSON response is expected.
[ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372 [ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372
## Upload an attachment to the wiki repository
Uploads a file to the attachment folder inside the wiki's repository. The
attachment folder is the `uploads` folder.
```
POST /projects/:id/wikis/attachments
```
| Attribute | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `file` | string | yes | The attachment to be uploaded |
| `branch` | string | no | The name of the branch. Defaults to the wiki repository default branch |
To upload a file from your filesystem, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded
by `@`. For example:
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "file=@dk.png" https://gitlab.example.com/api/v4/projects/1/wikis/attachments
```
Example response:
```json
{
"file_name" : "dk.png",
"file_path" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png",
"branch" : "master",
"link" : {
"url" : "uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png",
"markdown" : "![dk](uploads/6a061c4cf9f1c28cb22c384b4b8d4e3c/dk.png)"
}
}
```
...@@ -139,3 +139,11 @@ java: ...@@ -139,3 +139,11 @@ java:
- target/surefire-reports/TEST-*.xml - target/surefire-reports/TEST-*.xml
- target/failsafe-reports/TEST-*.xml - target/failsafe-reports/TEST-*.xml
``` ```
## Limitations
Currently, the following tools might not work because their XML formats are unsupported in GitLab.
|Case|Tool|Issue|
|---|---|---|
|`<testcase>` does not have `classname` attribute|ESlint, sass-lint|https://gitlab.com/gitlab-org/gitlab-ce/issues/50964|
...@@ -271,6 +271,8 @@ edit existing comments. Non-team members are restricted from adding or editing c ...@@ -271,6 +271,8 @@ edit existing comments. Non-team members are restricted from adding or editing c
| :-----------: | :----------: | | :-----------: | :----------: |
| ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) | | ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) |
Additionally locked issues can not be reopened.
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022 [ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125 [ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527 [ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
......
...@@ -10,6 +10,28 @@ module API ...@@ -10,6 +10,28 @@ module API
expose :content expose :content
end end
class WikiAttachment < Grape::Entity
include Gitlab::FileMarkdownLinkBuilder
expose :file_name
expose :file_path
expose :branch
expose :link do
expose :file_path, as: :url
expose :markdown do |_entity|
self.markdown_link
end
end
def filename
object.file_name
end
def secure_url
object.file_path
end
end
class UserSafe < Grape::Entity class UserSafe < Grape::Entity
expose :id, :name, :username expose :id, :name, :username
end end
......
module API module API
class Wikis < Grape::API class Wikis < Grape::API
helpers do helpers do
def commit_params(attrs)
{
file_name: attrs[:file][:filename],
file_content: File.read(attrs[:file][:tempfile]),
branch_name: attrs[:branch]
}
end
params :wiki_page_params do params :wiki_page_params do
requires :content, type: String, desc: 'Content of a wiki page' requires :content, type: String, desc: 'Content of a wiki page'
requires :title, type: String, desc: 'Title of a wiki page' requires :title, type: String, desc: 'Title of a wiki page'
...@@ -84,6 +92,29 @@ module API ...@@ -84,6 +92,29 @@ module API
status 204 status 204
WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page) WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
end end
desc 'Upload an attachment to the wiki repository' do
detail 'This feature was introduced in GitLab 11.3.'
success Entities::WikiAttachment
end
params do
requires :file, type: File, desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
authorize! :create_wiki, user_project
result = ::Wikis::CreateAttachmentService.new(user_project,
current_user,
commit_params(declared_params(include_missing: false))).execute
if result[:status] == :success
status(201)
present OpenStruct.new(result[:result]), with: Entities::WikiAttachment
else
render_api_error!(result[:message], 400)
end
end
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
require 'uri'
module Banzai module Banzai
module Filter module Filter
# HTML filter that "fixes" links to pages/files in a wiki. # HTML filter that "fixes" links to pages/files in a wiki.
...@@ -13,8 +11,12 @@ module Banzai ...@@ -13,8 +11,12 @@ module Banzai
def call def call
return doc unless project_wiki? return doc unless project_wiki?
doc.search('a:not(.gfm)').each do |el| doc.search('a:not(.gfm)').each { |el| process_link_attr(el.attribute('href')) }
process_link_attr el.attribute('href') doc.search('video').each { |el| process_link_attr(el.attribute('src')) }
doc.search('img').each do |el|
attr = el.attribute('data-src') || el.attribute('src')
process_link_attr(attr)
end end
doc doc
......
...@@ -10,11 +10,16 @@ module Banzai ...@@ -10,11 +10,16 @@ module Banzai
def apply_rules def apply_rules
# Special case: relative URLs beginning with `/uploads/` refer to # Special case: relative URLs beginning with `/uploads/` refer to
# user-uploaded files and will be handled elsewhere. # user-uploaded files will be handled elsewhere.
return @uri.to_s if @uri.relative? && @uri.path.starts_with?('/uploads/') return @uri.to_s if public_upload?
# Special case: relative URLs beginning with Wikis::CreateAttachmentService::ATTACHMENT_PATH
# refer to user-uploaded files to the wiki repository.
unless repository_upload?
apply_file_link_rules!
apply_hierarchical_link_rules!
end
apply_file_link_rules!
apply_hierarchical_link_rules!
apply_relative_link_rules! apply_relative_link_rules!
@uri.to_s @uri.to_s
end end
...@@ -39,6 +44,14 @@ module Banzai ...@@ -39,6 +44,14 @@ module Banzai
@uri = Addressable::URI.parse(link) @uri = Addressable::URI.parse(link)
end end
end end
def public_upload?
@uri.relative? && @uri.path.starts_with?('/uploads/')
end
def repository_upload?
@uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH)
end
end end
end end
end end
......
...@@ -2,18 +2,14 @@ module Gitlab ...@@ -2,18 +2,14 @@ module Gitlab
module Ci module Ci
module Parsers module Parsers
class Junit class Junit
attr_reader :data
JunitParserError = Class.new(StandardError) JunitParserError = Class.new(StandardError)
def parse!(xml_data, test_suite) def parse!(xml_data, test_suite)
@data = Hash.from_xml(xml_data) root = Hash.from_xml(xml_data)
each_suite do |testcases| all_cases(root) do |test_case|
testcases.each do |testcase| test_case = create_test_case(test_case)
test_case = create_test_case(testcase) test_suite.add_test_case(test_case)
test_suite.add_test_case(test_case)
end
end end
rescue REXML::ParseException => e rescue REXML::ParseException => e
raise JunitParserError, "XML parsing failed: #{e.message}" raise JunitParserError, "XML parsing failed: #{e.message}"
...@@ -23,26 +19,27 @@ module Gitlab ...@@ -23,26 +19,27 @@ module Gitlab
private private
def each_suite def all_cases(root, parent = nil, &blk)
testsuites.each do |testsuite| return unless root.present?
yield testcases(testsuite)
end
end
def testsuites [root].flatten.compact.map do |node|
if data['testsuites'] next unless node.is_a?(Hash)
data['testsuites']['testsuite']
else # we allow only one top-level 'testsuites'
[data['testsuite']] all_cases(node['testsuites'], root, &blk) unless parent
# we require at least one level of testsuites or testsuite
each_case(node['testcase'], &blk) if parent
# we allow multiple nested 'testsuite' (eg. PHPUnit)
all_cases(node['testsuite'], root, &blk)
end end
end end
def testcases(testsuite) def each_case(testcase, &blk)
if testsuite['testcase'].is_a?(Array) return unless testcase.present?
testsuite['testcase']
else [testcase].flatten.compact.map(&blk)
[testsuite['testcase']]
end
end end
def create_test_case(data) def create_test_case(data)
......
# Builds the markdown link of a file
# It needs the methods filename and secure_url (final destination url) to be defined.
module Gitlab
module FileMarkdownLinkBuilder
include FileTypeDetection
def markdown_link
return unless name = markdown_name
markdown = "[#{name.gsub(']', '\\]')}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
markdown
end
def markdown_name
return unless filename.present?
image_or_video? ? File.basename(filename, File.extname(filename)) : filename
end
end
end
# frozen_string_literal: true
# File helpers methods.
# It needs the method filename to be defined.
module Gitlab
module FileTypeDetection
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze
# These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
DANGEROUS_EXT = %w[svg].freeze
def image?
extension_match?(IMAGE_EXT)
end
def video?
extension_match?(VIDEO_EXT)
end
def image_or_video?
image? || video?
end
def dangerous?
extension_match?(DANGEROUS_EXT)
end
private
def extension_match?(extensions)
return false unless filename
extension = File.extname(filename).delete('.')
extensions.include?(extension.downcase)
end
end
end
...@@ -41,7 +41,9 @@ module ObjectStorage ...@@ -41,7 +41,9 @@ module ObjectStorage
GetURL: get_url, GetURL: get_url,
StoreURL: store_url, StoreURL: store_url,
DeleteURL: delete_url, DeleteURL: delete_url,
MultipartUpload: multipart_upload_hash MultipartUpload: multipart_upload_hash,
CustomPutHeaders: true,
PutHeaders: upload_options
}.compact }.compact
end end
......
...@@ -21,6 +21,34 @@ describe IssuableCollections do ...@@ -21,6 +21,34 @@ describe IssuableCollections do
controller controller
end end
describe '#set_set_order_from_cookie' do
describe 'when sort param given' do
let(:cookies) { {} }
let(:params) { { sort: 'downvotes_asc' } }
it 'sets the cookie with the right values and flags' do
allow(controller).to receive(:cookies).and_return(cookies)
controller.send(:set_sort_order_from_cookie)
expect(cookies['issue_sort']).to eq({ value: 'popularity', secure: false, httponly: false })
end
end
describe 'when cookie exists' do
let(:cookies) { { 'issue_sort' => 'id_asc' } }
let(:params) { {} }
it 'sets the cookie with the right values and flags' do
allow(controller).to receive(:cookies).and_return(cookies)
controller.send(:set_sort_order_from_cookie)
expect(cookies['issue_sort']).to eq({ value: 'created_asc', secure: false, httponly: false })
end
end
end
describe '#page_count_for_relation' do describe '#page_count_for_relation' do
let(:params) { { state: 'opened' } } let(:params) { { state: 'opened' } }
......
...@@ -146,6 +146,8 @@ describe "User creates wiki page" do ...@@ -146,6 +146,8 @@ describe "User creates wiki page" do
expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
end end
end end
it_behaves_like 'wiki file attachments'
end end
context "in a group namespace", :js do context "in a group namespace", :js do
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'User updates wiki page' do describe 'User updates wiki page' do
shared_examples 'wiki page user update' do shared_examples 'wiki page user update' do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
...@@ -55,6 +56,8 @@ describe 'User updates wiki page' do ...@@ -55,6 +56,8 @@ describe 'User updates wiki page' do
expect(page).to have_content('Updated Wiki Content') expect(page).to have_content('Updated Wiki Content')
end end
it_behaves_like 'wiki file attachments'
end end
end end
...@@ -64,14 +67,14 @@ describe 'User updates wiki page' do ...@@ -64,14 +67,14 @@ describe 'User updates wiki page' do
before do before do
visit(project_wikis_path(project)) visit(project_wikis_path(project))
click_link('Edit')
end end
context 'in a user namespace' do context 'in a user namespace' do
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'updates a page' do it 'updates a page' do
click_link('Edit')
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home') expect(page).to have_field('wiki[message]', with: 'Update home')
...@@ -84,8 +87,6 @@ describe 'User updates wiki page' do ...@@ -84,8 +87,6 @@ describe 'User updates wiki page' do
end end
it 'shows a validation error message' do it 'shows a validation error message' do
click_link('Edit')
fill_in(:wiki_content, with: '') fill_in(:wiki_content, with: '')
click_button('Save changes') click_button('Save changes')
...@@ -97,8 +98,6 @@ describe 'User updates wiki page' do ...@@ -97,8 +98,6 @@ describe 'User updates wiki page' do
end end
it 'shows the emoji autocompletion dropdown', :js do it 'shows the emoji autocompletion dropdown', :js do
click_link('Edit')
find('#wiki_content').native.send_keys('') find('#wiki_content').native.send_keys('')
fill_in(:wiki_content, with: ':') fill_in(:wiki_content, with: ':')
...@@ -106,8 +105,6 @@ describe 'User updates wiki page' do ...@@ -106,8 +105,6 @@ describe 'User updates wiki page' do
end end
it 'shows the error message' do it 'shows the error message' do
click_link('Edit')
wiki_page.update(content: 'Update') wiki_page.update(content: 'Update')
click_button('Save changes') click_button('Save changes')
...@@ -116,30 +113,27 @@ describe 'User updates wiki page' do ...@@ -116,30 +113,27 @@ describe 'User updates wiki page' do
end end
it 'updates a page' do it 'updates a page' do
click_on('Edit')
fill_in('Content', with: 'Updated Wiki Content') fill_in('Content', with: 'Updated Wiki Content')
click_on('Save changes') click_on('Save changes')
expect(page).to have_content('Updated Wiki Content') expect(page).to have_content('Updated Wiki Content')
end end
it 'cancels edititng of a page' do it 'cancels editing of a page' do
click_on('Edit')
page.within(:css, '.wiki-form .form-actions') do page.within(:css, '.wiki-form .form-actions') do
click_on('Cancel') click_on('Cancel')
end end
expect(current_path).to eq(project_wiki_path(project, wiki_page)) expect(current_path).to eq(project_wiki_path(project, wiki_page))
end end
it_behaves_like 'wiki file attachments'
end end
context 'in a group namespace' do context 'in a group namespace' do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it 'updates a page' do it 'updates a page' do
click_link('Edit')
# Commit message field should have correct value. # Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home') expect(page).to have_field('wiki[message]', with: 'Update home')
...@@ -151,6 +145,8 @@ describe 'User updates wiki page' do ...@@ -151,6 +145,8 @@ describe 'User updates wiki page' do
expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content("Last edited by #{user.name}")
expect(page).to have_content('My awesome wiki!') expect(page).to have_content('My awesome wiki!')
end end
it_behaves_like 'wiki file attachments'
end end
end end
...@@ -222,6 +218,8 @@ describe 'User updates wiki page' do ...@@ -222,6 +218,8 @@ describe 'User updates wiki page' do
expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
end end
it_behaves_like 'wiki file attachments'
end end
end end
......
...@@ -93,7 +93,7 @@ describe 'User views a wiki page' do ...@@ -93,7 +93,7 @@ describe 'User views a wiki page' do
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg') allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file) allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath('//img[@data-src="image.jpg"]') expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/image.jpg']")
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
click_on('image') click_on('image')
......
...@@ -7,6 +7,7 @@ describe Banzai::Filter::WikiLinkFilter do ...@@ -7,6 +7,7 @@ describe Banzai::Filter::WikiLinkFilter do
let(:project) { build_stubbed(:project, :public, name: "wiki_link_project", namespace: namespace) } let(:project) { build_stubbed(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:user) { double } let(:user) { double }
let(:wiki) { ProjectWiki.new(project, user) } let(:wiki) { ProjectWiki.new(project, user) }
let(:repository_upload_folder) { Wikis::CreateAttachmentService::ATTACHMENT_PATH }
it "doesn't rewrite absolute links" do it "doesn't rewrite absolute links" do
filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0] filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0]
...@@ -20,6 +21,45 @@ describe Banzai::Filter::WikiLinkFilter do ...@@ -20,6 +21,45 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('href').value).to eq('/uploads/a.test') expect(filtered_link.attribute('href').value).to eq('/uploads/a.test')
end end
describe "when links point to the #{Wikis::CreateAttachmentService::ATTACHMENT_PATH} folder" do
context 'with an "a" html tag' do
it 'rewrites links' do
filtered_link = filter("<a href='#{repository_upload_folder}/a.test'>Link</a>", project_wiki: wiki).children[0]
expect(filtered_link.attribute('href').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.test")
end
end
context 'with "img" html tag' do
let(:path) { "#{wiki.wiki_base_path}/#{repository_upload_folder}/a.jpg" }
context 'inside an "a" html tag' do
it 'rewrites links' do
filtered_elements = filter("<a href='#{repository_upload_folder}/a.jpg'><img src='#{repository_upload_folder}/a.jpg'>example</img></a>", project_wiki: wiki)
expect(filtered_elements.search('img').first.attribute('src').value).to eq(path)
expect(filtered_elements.search('a').first.attribute('href').value).to eq(path)
end
end
context 'outside an "a" html tag' do
it 'rewrites links' do
filtered_link = filter("<img src='#{repository_upload_folder}/a.jpg'>example</img>", project_wiki: wiki).children[0]
expect(filtered_link.attribute('src').value).to eq(path)
end
end
end
context 'with "video" html tag' do
it 'rewrites links' do
filtered_link = filter("<video src='#{repository_upload_folder}/a.mp4'></video>", project_wiki: wiki).children[0]
expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4")
end
end
end
describe "invalid links" do describe "invalid links" do
invalid_links = ["http://:8080", "http://", "http://:8080/path"] invalid_links = ["http://:8080", "http://", "http://:8080/path"]
......
require 'spec_helper' require 'fast_spec_helper'
describe Gitlab::Ci::Parsers::Junit do describe Gitlab::Ci::Parsers::Junit do
describe '#parse!' do describe '#parse!' do
...@@ -8,21 +8,35 @@ describe Gitlab::Ci::Parsers::Junit do ...@@ -8,21 +8,35 @@ describe Gitlab::Ci::Parsers::Junit do
let(:test_cases) { flattened_test_cases(test_suite) } let(:test_cases) { flattened_test_cases(test_suite) }
context 'when data is JUnit style XML' do context 'when data is JUnit style XML' do
context 'when there are no test cases' do context 'when there are no <testcases> in <testsuite>' do
let(:junit) do let(:junit) do
<<-EOF.strip_heredoc <<-EOF.strip_heredoc
<testsuite></testsuite> <testsuite></testsuite>
EOF EOF
end end
it 'raises an error and does not add any test cases' do it 'ignores the case' do
expect { subject }.to raise_error(described_class::JunitParserError) expect { subject }.not_to raise_error
expect(test_cases.count).to eq(0)
end
end
context 'when there are no <testcases> in <testsuites>' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuites><testsuite /></testsuites>
EOF
end
it 'ignores the case' do
expect { subject }.not_to raise_error
expect(test_cases.count).to eq(0) expect(test_cases.count).to eq(0)
end end
end end
context 'when there is a test case' do context 'when there is only one <testcase> in <testsuite>' do
let(:junit) do let(:junit) do
<<-EOF.strip_heredoc <<-EOF.strip_heredoc
<testsuite> <testsuite>
...@@ -40,6 +54,46 @@ describe Gitlab::Ci::Parsers::Junit do ...@@ -40,6 +54,46 @@ describe Gitlab::Ci::Parsers::Junit do
end end
end end
context 'when there is only one <testsuite> in <testsuites>' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuites>
<testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
</testsuite>
</testsuites>
EOF
end
it 'parses XML and adds a test case to a suite' do
expect { subject }.not_to raise_error
expect(test_cases[0].classname).to eq('Calculator')
expect(test_cases[0].name).to eq('sumTest1')
expect(test_cases[0].execution_time).to eq(0.01)
end
end
context 'PHPUnit' do
let(:junit) do
<<-EOF.strip_heredoc
<testsuites>
<testsuite name="Project Test Suite" tests="1" assertions="1" failures="0" errors="0" time="1.376748">
<testsuite name="XXX\\FrontEnd\\WebBundle\\Tests\\Controller\\LogControllerTest" file="/Users/mcfedr/projects/xxx/server/tests/XXX/FrontEnd/WebBundle/Tests/Controller/LogControllerTest.php" tests="1" assertions="1" failures="0" errors="0" time="1.376748">
<testcase name="testIndexAction" class="XXX\\FrontEnd\\WebBundle\\Tests\\Controller\\LogControllerTest" file="/Users/mcfedr/projects/xxx/server/tests/XXX/FrontEnd/WebBundle/Tests/Controller/LogControllerTest.php" line="9" assertions="1" time="1.376748"/>
</testsuite>
</testsuite>
</testsuites>
EOF
end
it 'parses XML and adds a test case to a suite' do
expect { subject }.not_to raise_error
expect(test_cases.count).to eq(1)
end
end
context 'when there are two test cases' do context 'when there are two test cases' do
let(:junit) do let(:junit) do
<<-EOF.strip_heredoc <<-EOF.strip_heredoc
......
# frozen_string_literal: true
require 'rails_helper'
describe Gitlab::FileMarkdownLinkBuilder do
let(:custom_class) do
Class.new do
include Gitlab::FileMarkdownLinkBuilder
end.new
end
before do
allow(custom_class).to receive(:filename).and_return(filename)
end
describe 'markdown_link' do
let(:url) { "/uploads/#{filename}"}
before do
allow(custom_class).to receive(:secure_url).and_return(url)
end
context 'when file name has the character ]' do
let(:filename) { 'd]k.png' }
it 'escapes the character' do
expect(custom_class.markdown_link).to eq '![d\\]k](/uploads/d]k.png)'
end
end
context 'when file is an image or video' do
let(:filename) { 'dk.png' }
it 'returns preview markdown link' do
expect(custom_class.markdown_link).to eq '![dk](/uploads/dk.png)'
end
end
context 'when file is not an image or video' do
let(:filename) { 'dk.zip' }
it 'returns markdown link' do
expect(custom_class.markdown_link).to eq '[dk.zip](/uploads/dk.zip)'
end
end
context 'when file name is blank' do
let(:filename) { nil }
it 'returns nil' do
expect(custom_class.markdown_link).to eq nil
end
end
end
describe 'mardown_name' do
context 'when file is an image or video' do
let(:filename) { 'dk.png' }
it 'retrieves the name without the extension' do
expect(custom_class.markdown_name).to eq 'dk'
end
end
context 'when file is not an image or video' do
let(:filename) { 'dk.zip' }
it 'retrieves the name with the extesion' do
expect(custom_class.markdown_name).to eq 'dk.zip'
end
end
context 'when file name is blank' do
let(:filename) { nil }
it 'returns nil' do
expect(custom_class.markdown_name).to eq nil
end
end
end
end
# frozen_string_literal: true
require 'rails_helper'
describe Gitlab::FileTypeDetection do
def upload_fixture(filename)
fixture_file_upload(File.join('spec', 'fixtures', filename))
end
describe '#image_or_video?' do
context 'when class is an uploader' do
let(:uploader) do
example_uploader = Class.new(CarrierWave::Uploader::Base) do
include Gitlab::FileTypeDetection
storage :file
end
example_uploader.new
end
it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
expect(uploader).to be_image_or_video
end
it 'returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
expect(uploader).to be_image_or_video
end
it 'returns false for other extensions' do
uploader.store!(upload_fixture('doc_sample.txt'))
expect(uploader).not_to be_image_or_video
end
it 'returns false if filename is blank' do
uploader.store!(upload_fixture('dk.png'))
allow(uploader).to receive(:filename).and_return(nil)
expect(uploader).not_to be_image_or_video
end
end
context 'when class is a regular class' do
let(:custom_class) do
custom_class = Class.new do
include Gitlab::FileTypeDetection
end
custom_class.new
end
it 'returns true for an image file' do
allow(custom_class).to receive(:filename).and_return('dk.png')
expect(custom_class).to be_image_or_video
end
it 'returns true for a video file' do
allow(custom_class).to receive(:filename).and_return('video_sample.mp4')
expect(custom_class).to be_image_or_video
end
it 'returns false for other extensions' do
allow(custom_class).to receive(:filename).and_return('doc_sample.txt')
expect(custom_class).not_to be_image_or_video
end
it 'returns false if filename is blank' do
allow(custom_class).to receive(:filename).and_return(nil)
expect(custom_class).not_to be_image_or_video
end
end
end
end
...@@ -61,6 +61,8 @@ describe ObjectStorage::DirectUpload do ...@@ -61,6 +61,8 @@ describe ObjectStorage::DirectUpload do
expect(subject[:GetURL]).to start_with(storage_url) expect(subject[:GetURL]).to start_with(storage_url)
expect(subject[:StoreURL]).to start_with(storage_url) expect(subject[:StoreURL]).to start_with(storage_url)
expect(subject[:DeleteURL]).to start_with(storage_url) expect(subject[:DeleteURL]).to start_with(storage_url)
expect(subject[:CustomPutHeaders]).to be_truthy
expect(subject[:PutHeaders]).to eq({ 'Content-Type' => 'application/octet-stream' })
end end
end end
......
...@@ -112,6 +112,7 @@ describe IssuePolicy do ...@@ -112,6 +112,7 @@ describe IssuePolicy do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) } let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
let(:issue_no_assignee) { create(:issue, project: project) } let(:issue_no_assignee) { create(:issue, project: project) }
let(:issue_locked) { create(:issue, project: project, discussion_locked: true, author: author, assignees: [assignee]) }
before do before do
project.add_guest(guest) project.add_guest(guest)
...@@ -124,36 +125,49 @@ describe IssuePolicy do ...@@ -124,36 +125,49 @@ describe IssuePolicy do
it 'allows guests to read issues' do it 'allows guests to read issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid)
expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
expect(permissions(guest, issue_locked)).to be_allowed(:read_issue, :read_issue_iid)
expect(permissions(guest, issue_locked)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
end end
it 'allows reporters to read, update, and admin issues' do it 'allows reporters to read, update, reopen, and admin issues' do
expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
expect(permissions(reporter, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
expect(permissions(reporter, issue_locked)).to be_disallowed(:reopen_issue)
end end
it 'allows reporters from group links to read, update, and admin issues' do it 'allows reporters from group links to read, update, reopen and admin issues' do
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
end end
it 'allows issue authors to read and update their issues' do it 'allows issue authors to read, reopen and update their issues' do
expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
expect(permissions(author, issue)).to be_disallowed(:admin_issue) expect(permissions(author, issue)).to be_disallowed(:admin_issue)
expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
expect(permissions(author, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
expect(permissions(author, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue)
end end
it 'allows issue assignees to read and update their issues' do it 'allows issue assignees to read, reopen and update their issues' do
expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) expect(permissions(assignee, issue)).to be_disallowed(:admin_issue)
expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
expect(permissions(assignee, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
expect(permissions(assignee, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue)
end end
context 'with confidential issues' do context 'with confidential issues' do
......
...@@ -139,6 +139,27 @@ describe API::Wikis do ...@@ -139,6 +139,27 @@ describe API::Wikis do
end end
end end
shared_examples_for 'uploads wiki attachment' do
it 'pushes attachment to the wiki repository' do
allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
post(api(url, user), payload)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq result_hash.deep_stringify_keys
end
it 'responds with validation error on empty file' do
payload.delete(:file)
post(api(url, user), payload)
expect(response).to have_gitlab_http_status(400)
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is missing')
end
end
describe 'GET /projects/:id/wikis' do describe 'GET /projects/:id/wikis' do
let(:url) { "/projects/#{project.id}/wikis" } let(:url) { "/projects/#{project.id}/wikis" }
...@@ -698,4 +719,107 @@ describe API::Wikis do ...@@ -698,4 +719,107 @@ describe API::Wikis do
include_examples '204 No Content' include_examples '204 No Content'
end end
end end
describe 'POST /projects/:id/wikis/attachments' do
let(:payload) { { file: fixture_file_upload('spec/fixtures/dk.png') } }
let(:url) { "/projects/#{project.id}/wikis/attachments" }
let(:file_path) { "#{Wikis::CreateAttachmentService::ATTACHMENT_PATH}/fixed_hex/dk.png" }
let(:result_hash) do
{
file_name: 'dk.png',
file_path: file_path,
branch: 'master',
link: {
url: file_path,
markdown: "![dk](#{file_path})"
}
}
end
context 'when wiki is disabled' do
let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
post(api(url), payload)
end
include_examples '404 Project Not Found'
end
context 'when user is developer' do
before do
project.add_developer(user)
post(api(url, user), payload)
end
include_examples '403 Forbidden'
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
post(api(url, user), payload)
end
include_examples '403 Forbidden'
end
end
context 'when wiki is available only for team members' do
let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
post(api(url), payload)
end
include_examples '404 Project Not Found'
end
context 'when user is developer' do
before do
project.add_developer(user)
end
include_examples 'uploads wiki attachment'
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
include_examples 'uploads wiki attachment'
end
end
context 'when wiki is available for everyone with access' do
let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
post(api(url), payload)
end
include_examples '404 Project Not Found'
end
context 'when user is developer' do
before do
project.add_developer(user)
end
include_examples 'uploads wiki attachment'
end
context 'when user is maintainer' do
before do
project.add_maintainer(user)
end
include_examples 'uploads wiki attachment'
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Wikis::CreateAttachmentService do
let(:project) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:file_name) { 'filename.txt' }
let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} }
let(:file_opts) do
{
file_name: file_name,
file_content: 'Content of attachment'
}
end
let(:opts) { file_opts }
subject(:service) { described_class.new(project, user, opts) }
before do
project.add_developer(user)
end
describe 'initialization' do
context 'author commit info' do
it 'does not raise error if user is nil' do
service = described_class.new(project, nil, opts)
expect(service.instance_variable_get(:@author_email)).to be_nil
expect(service.instance_variable_get(:@author_name)).to be_nil
end
it 'fills file_path from the repository uploads folder' do
expect(service.instance_variable_get(:@file_path)).to match(file_path_regex)
end
context 'when no author info provided' do
it 'fills author_email and author_name from current_user info' do
expect(service.instance_variable_get(:@author_email)).to eq user.email
expect(service.instance_variable_get(:@author_name)).to eq user.name
end
end
context 'when author info provided' do
let(:author_email) { 'author_email' }
let(:author_name) { 'author_name' }
let(:opts) { file_opts.merge(author_email: author_email, author_name: author_name) }
it 'fills author_email and author_name from params' do
expect(service.instance_variable_get(:@author_email)).to eq author_email
expect(service.instance_variable_get(:@author_name)).to eq author_name
end
end
end
context 'commit message' do
context 'when no commit message provided' do
it 'sets a default commit message' do
expect(service.instance_variable_get(:@commit_message)).to eq "Upload attachment #{opts[:file_name]}"
end
end
context 'when commit message provided' do
let(:commit_message) { 'whatever' }
let(:opts) { file_opts.merge(commit_message: commit_message) }
it 'use the commit message from params' do
expect(service.instance_variable_get(:@commit_message)).to eq commit_message
end
end
end
context 'branch name' do
context 'when no branch provided' do
it 'sets the branch from the wiki default_branch' do
expect(service.instance_variable_get(:@branch_name)).to eq project.wiki.default_branch
end
end
context 'when branch provided' do
let(:branch_name) { 'whatever' }
let(:opts) { file_opts.merge(branch_name: branch_name) }
it 'use the commit message from params' do
expect(service.instance_variable_get(:@branch_name)).to eq branch_name
end
end
end
end
describe 'validations' do
context 'when file_name' do
context 'is not present' do
let(:file_name) { nil }
it 'returns error' do
result = service.execute
expect(result[:status]).to eq :error
expect(result[:message]).to eq 'The file name cannot be empty'
end
end
context 'length' do
context 'is bigger than 255' do
let(:file_name) { "#{'0' * 256}.jpg" }
it 'truncates file name' do
result = service.execute
expect(result[:status]).to eq :success
expect(result[:result][:file_name].length).to eq 255
expect(result[:result][:file_name]).to match(/0{251}\.jpg/)
end
end
context 'is less or equal to 255 does not return error' do
let(:file_name) { '0' * 255 }
it 'does not return error' do
result = service.execute
expect(result[:status]).to eq :success
end
end
end
end
context 'when user' do
shared_examples 'wiki attachment user validations' do
it 'returns error' do
result = described_class.new(project, user2, opts).execute
expect(result[:status]).to eq :error
expect(result[:message]).to eq 'You are not allowed to push to the wiki'
end
end
context 'does not have permission' do
let(:user2) { create(:user) }
it_behaves_like 'wiki attachment user validations'
end
context 'is nil' do
let(:user2) { nil }
it_behaves_like 'wiki attachment user validations'
end
end
end
describe '#execute' do
let(:wiki) { project.wiki }
subject(:service_execute) { service.execute[:result] }
context 'creates branch if it does not exists' do
let(:branch_name) { 'new_branch' }
let(:opts) { file_opts.merge(branch_name: branch_name) }
it do
expect(wiki.repository.branches).to be_empty
expect { service.execute }.to change { wiki.repository.branches.count }.by(1)
expect(wiki.repository.branches.first.name).to eq branch_name
end
end
it 'adds file to the repository' do
expect(wiki.repository.ls_files('HEAD')).to be_empty
service.execute
files = wiki.repository.ls_files('HEAD')
expect(files.count).to eq 1
expect(files.first).to match(file_path_regex)
end
context 'returns' do
before do
allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
service_execute
end
it 'returns the file name' do
expect(service_execute[:file_name]).to eq file_name
end
it 'returns the path where file was stored' do
expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt'
end
it 'returns the branch where the file was pushed' do
expect(service_execute[:branch]).to eq wiki.default_branch
end
it 'returns the commit id' do
expect(service_execute[:commit]).not_to be_empty
end
end
end
end
# frozen_string_literal: true
# Requires a context containing:
# project
shared_examples 'wiki file attachments' do
include DropzoneHelper
context 'uploading attachments', :js do
let(:wiki) { project.wiki }
def attach_with_dropzone(wait = false)
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, wait)
end
context 'before uploading' do
it 'shows "Attach a file" button' do
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
context 'uploading is in progress' do
it 'cancels uploading on clicking to "Cancel" button' do
slow_requests do
attach_with_dropzone
click_button 'Cancel'
end
expect(page).to have_button('Attach a file')
expect(page).not_to have_button('Cancel')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
it 'shows "Attaching a file" message on uploading 1 file' do
slow_requests do
attach_with_dropzone
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
end
end
end
context 'uploading is complete' do
it 'shows "Attach a file" button on uploading complete' do
attach_with_dropzone
wait_for_requests
expect(page).to have_button('Attach a file')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
it 'the markdown link is added to the page' do
fill_in(:wiki_content, with: '')
attach_with_dropzone(true)
wait_for_requests
expect(page.find('#wiki_content').value)
.to match(%r{\!\[dk\]\(uploads/\h{32}/dk\.png\)$})
end
it 'the links point to the wiki root url' do
attach_with_dropzone(true)
wait_for_requests
find('.js-md-preview-button').click
file_path = page.find('input[name="files[]"]', visible: :hidden).value
link = page.find('a.no-attachment-icon')['href']
img_link = page.find('a.no-attachment-icon img')['src']
expect(link).to eq img_link
expect(URI.parse(link).path).to eq File.join(wiki.wiki_base_path, file_path)
end
it 'the file has been added to the wiki repository' do
expect do
attach_with_dropzone(true)
wait_for_requests
end.to change { wiki.repository.ls_files('HEAD').count }.by(1)
file_path = page.find('input[name="files[]"]', visible: :hidden).value
expect(wiki.find_file(file_path, 'HEAD').path).not_to be_nil
end
end
end
end
...@@ -11,27 +11,10 @@ describe UploaderHelper do ...@@ -11,27 +11,10 @@ describe UploaderHelper do
example_uploader.new example_uploader.new
end end
def upload_fixture(filename) describe '#extension_match?' do
fixture_file_upload(File.join('spec', 'fixtures', filename)) it 'returns false if file does not exists' do
end expect(uploader.file).to be_nil
expect(uploader.send(:extension_match?, 'jpg')).to eq false
describe '#image_or_video?' do
it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
expect(uploader).to be_image_or_video
end
it 'it returns true for a video file' do
uploader.store!(upload_fixture('video_sample.mp4'))
expect(uploader).to be_image_or_video
end
it 'returns false for other extensions' do
uploader.store!(upload_fixture('doc_sample.txt'))
expect(uploader).not_to be_image_or_video
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment