New upstream version 8.12.3+dfsg1

This commit is contained in:
Praveen Arimbrathodiyil 2016-10-01 15:18:49 +05:30
parent ab0093fc8a
commit 4d449cfe16
72 changed files with 554 additions and 186 deletions

View file

@ -1,6 +1,24 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.12.2 (unreleased)
v 8.12.4 (unreleased)
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2
- Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination
- Fix List-Unsubscribe header in emails
- Fix IssuesController#show degradation including project on loaded notes
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513
- Fix errors importing project feature and milestone models using GitLab project import
- Make JWT messages Docker-compatible
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513
- Fix duplicate branch entry in the merge request version compare dropdown
- Respect the fork_project permission when forking projects
- Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path
v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST

View file

@ -1 +1 @@
3.6.0
3.6.1

View file

@ -1 +1 @@
8.12.1
8.12.3

View file

@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
labelsPath: "/api/:version/projects/:id/labels",
labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
@ -65,13 +65,14 @@
return callback(projects);
});
},
newLabel: function(project_id, data, callback) {
newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id);
.replace(':namespace_path', namespace_path)
.replace(':project_path', project_path);
return $.ajax({
url: url,
type: "POST",
data: data,
data: {'label': data},
dataType: "json"
}).done(function(label) {
return callback(label);

View file

@ -357,7 +357,7 @@
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
// Generate a search result block
h5 = $('<h5>').text('Search results');
h5 = $('<h5 class="emoji-search" />').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();

View file

@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {

View file

@ -1,8 +1,9 @@
(function (w) {
class CreateLabelDropdown {
constructor ($el, projectId) {
constructor ($el, namespacePath, projectPath) {
this.$el = $el;
this.projectId = projectId;
this.namespacePath = namespacePath;
this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el);
@ -91,8 +92,8 @@
e.preventDefault();
e.stopPropagation();
Api.newLabel(this.projectId, {
name: this.$newLabelField.val(),
Api.newLabel(this.namespacePath, this.projectPath, {
title: this.$newLabelField.val(),
color: this.$newColorField.val()
}, (label) => {
this.$newLabelCreateButton.enable();

View file

@ -1,13 +1,9 @@
((w) => {
w.ResolveBtn = Vue.extend({
mixins: [
ButtonMixins
],
props: {
noteId: Number,
discussionId: String,
resolved: Boolean,
namespacePath: String,
projectPath: String,
canResolve: Boolean,
resolvedBy: String
@ -69,10 +65,10 @@
if (this.isResolved) {
promise = ResolveService
.unresolve(this.namespace, this.noteId);
.unresolve(this.projectPath, this.noteId);
} else {
promise = ResolveService
.resolve(this.namespace, this.noteId);
.resolve(this.projectPath, this.noteId);
}
promise.then((response) => {

View file

@ -1,12 +1,8 @@
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
mixins: [
ButtonMixins
],
props: {
discussionId: String,
mergeRequestId: Number,
namespacePath: String,
projectPath: String,
canResolve: Boolean,
},
@ -50,7 +46,7 @@
},
methods: {
resolve: function () {
ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
}
},
created: function () {

View file

@ -1,9 +0,0 @@
((w) => {
w.ButtonMixins = {
computed: {
namespace: function () {
return `${this.namespacePath}/${this.projectPath}`;
}
}
};
})(window);

View file

@ -9,32 +9,32 @@
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
}
prepareRequest(namespace) {
prepareRequest(root) {
this.setCSRF();
Vue.http.options.root = `/${namespace}`;
Vue.http.options.root = root;
}
resolve(namespace, noteId) {
this.prepareRequest(namespace);
resolve(projectPath, noteId) {
this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {});
}
unresolve(namespace, noteId) {
this.prepareRequest(namespace);
unresolve(projectPath, noteId) {
this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {});
}
toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId],
isResolved = discussion.isResolved();
let promise;
if (isResolved) {
promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
} else {
promise = this.resolveAll(namespace, mergeRequestId, discussionId);
promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
}
promise.then((response) => {
@ -57,10 +57,10 @@
})
}
resolveAll(namespace, mergeRequestId, discussionId) {
resolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
this.prepareRequest(namespace);
this.prepareRequest(projectPath);
discussion.loading = true;
@ -70,10 +70,10 @@
}, {});
}
unResolveAll(namespace, mergeRequestId, discussionId) {
unResolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
this.prepareRequest(namespace);
this.prepareRequest(projectPath);
discussion.loading = true;

View file

@ -4,9 +4,10 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
namespacePath = $dropdown.data('namespace-path');
projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
@ -24,6 +25,11 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('field-name') + '"]')
.map(function () {
return this.value;
}).get();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
}
@ -35,7 +41,7 @@
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
@ -43,6 +49,10 @@
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
return this.value;
}).get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;

View file

@ -19,7 +19,7 @@
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
values.push(sParameterName[1]);
values.push(sParameterName[1].replace(/\+/g, ' '));
}
i++;
}

View file

@ -432,14 +432,12 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
var namespacePath = $form.attr('data-namespace-path'),
projectPath = $form.attr('data-project-path')
discussionId = $form.attr('data-discussion-id'),
mergeRequestId = $form.attr('data-noteable-iid'),
namespace = namespacePath + '/' + projectPath;
var projectPath = $form.data('project-path')
discussionId = $form.data('discussion-id'),
mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
}
}
@ -854,7 +852,6 @@
.closest('form')
.attr('data-discussion-id', discussionId)
.attr('data-resolve-all', 'true')
.attr('data-namespace-path', $this.attr('data-namespace-path'))
.attr('data-project-path', $this.attr('data-project-path'));
};

View file

@ -25,7 +25,7 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_403 unless @authentication_result.success? &&
render_unauthorized unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
rescue Gitlab::Auth::MissingPersonalTokenError
@ -33,10 +33,21 @@ class JwtController < ApplicationController
end
def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: 401
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
] }, status: 401
end
def render_unauthorized
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
] }, status: 401
end
def auth_params

View file

@ -54,7 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
raw_notes = @issue.notes_with_associations.fresh
raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)

View file

@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params)
if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project)
respond_to do |format|
format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
format.json { render json: @label }
end
else
render 'new'
respond_to do |format|
format.html { render 'new' }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end
end

View file

@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("snippets/_snippets", collection: @snippets)
html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
}
end
end

View file

@ -109,7 +109,7 @@ class Notify < BaseMailer
headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true)
headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end

View file

@ -10,15 +10,33 @@ class CycleAnalytics
end
def commits
repository = @project.repository.raw_repository
if @project.default_branch
repository.log(ref: @project.default_branch, after: @from).count
end
ref = @project.default_branch.presence
count_commits_for(ref)
end
def deploys
@project.deployments.where("created_at > ?", @from).count
end
private
# Don't use the `Gitlab::Git::Repository#log` method, because it enforces
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
# the easiest way forward is to replicate the relevant portions of the
# `log` function here.
def count_commits_for(ref)
return unless ref
repository = @project.repository.raw_repository
sha = @project.repository.commit(ref).sha
cmd = %W(git --git-dir=#{repository.path} log)
cmd << '--format=%H'
cmd << "--after=#{@from.iso8601}"
cmd << sha
raw_output = IO.popen(cmd) { |io| io.read }
raw_output.lines.count
end
end
end

View file

@ -9,6 +9,10 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
def title=(value)
self.properties['title'] = value if self.properties
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']

View file

@ -7,10 +7,10 @@ module Auth
def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
return error('not found', 404) unless registry.enabled
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project
return error('forbidden', 403) unless scope
return error('DENIED', status: 403, message: 'access forbidden') unless scope
end
{ token: authorized_token(scope).encoded }
@ -111,5 +111,12 @@ module Auth
@authentication_abilities.include?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
def error(code, status:, message: '')
{
errors: [{ code: code, message: message }],
http_status: status
}
end
end
end

View file

@ -15,6 +15,11 @@ module Projects
return @project
end
unless allowed_fork?(forked_from_project_id)
@project.errors.add(:forked_from_project_id, 'is forbidden')
return @project
end
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
@ -71,6 +76,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
def allowed_fork?(source_project_id)
return true if source_project_id.nil?
source_project = Project.find_by(id: source_project_id)
current_user.can?(:fork_project, source_project)
end
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)

View file

@ -16,6 +16,8 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)

View file

@ -16,8 +16,7 @@
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
%pre
= simple_format build[:commands]
%pre= build[:commands]
%br
%b Tag list:

View file

@ -1,4 +1,4 @@
= form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- if devise_mapping.rememberable?

View file

@ -1,11 +1,10 @@
- if discussion.for_merge_request?
%resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'",
":project-path" => "'#{discussion.project.path}'",
%resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" }
%button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading" }
%button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
= icon("spinner spin", "v-show" => "loading")
{{ buttonText }}

View file

@ -8,13 +8,7 @@
%tbody
%th Status
%th Commit
- pipelines.stages.each do |stage|
%th.stage
- if stage.titleize.length > 12
%span.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize
- else
= stage.titleize
%th Stages
%th
%th
= render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true

View file

@ -5,7 +5,7 @@
- if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
%button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } }
%button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
#notes= render "projects/notes/notes_with_form"

View file

@ -58,11 +58,11 @@
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
%strong
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
%strong
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block

View file

@ -24,14 +24,12 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'",
":project-path" => "'#{note.project.path}'",
":discussion-id" => "'#{note.discussion_id}'",
%resolve-btn{ "project-path" => "#{project_path(note.project)}",
"discussion-id" => "#{note.discussion_id}",
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":resolved-by" => "'#{note.resolved_by.try(:name)}'",
"resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"v-ref:note_#{note.id}" => true }

View file

@ -37,7 +37,7 @@
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.dropdown.pull-right
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
%button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Create new list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }

View file

@ -4,7 +4,7 @@
- show_footer = local_assigns.fetch(:show_footer, true)
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Label"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit

View file

@ -128,7 +128,7 @@
- issuable.labels_array.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
%button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
%span.dropdown-toggle-text
Label
= icon('chevron-down')

View file

@ -1,3 +1,5 @@
- remote = local_assigns.fetch(:remote, false)
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
@ -5,7 +7,7 @@
%li
.nothing-here-block Nothing here.
= paginate @snippets, theme: 'gitlab', remote: true
= paginate @snippets, theme: 'gitlab', remote: remote
:javascript
gl.SnippetsList();

View file

@ -99,13 +99,24 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
config.middleware.use Rack::Attack
config.middleware.insert_before Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains
config.middleware.use Rack::Cors do
config.middleware.insert_before Warden::Manager, Rack::Cors do
allow do
origins Gitlab.config.gitlab.url
resource '/api/*',
credentials: true,
headers: :any,
methods: :any,
expose: ['Link']
end
# Cross-origin requests must not have the session cookie available
allow do
origins '*'
resource '/api/*',
credentials: false,
headers: :any,
methods: :any,
expose: ['Link']

View file

@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
@project.team << [user, :reporter]
@forked_project = Projects::ForkService.new(@project, user).execute
end

View file

@ -21,8 +21,11 @@ module API
end
# Check the Rails session for valid authentication details
#
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden
warden ? warden.authenticate : nil
warden.try(:authenticate) if request.get? || request.head?
end
def find_user_by_private_token

View file

@ -90,7 +90,7 @@ module API
{
username: token_handler.actor_name,
lfs_token: token_handler.generate,
lfs_token: token_handler.token,
repository_http_path: project.http_url_to_repo
}
end

View file

@ -124,7 +124,7 @@ module Gitlab
read_authentication_abilities
end
Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.value, password)
Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
end
def build_access_token_check(login, password)

View file

@ -1,5 +1,8 @@
# Model relationships to be included in the project import/export
project_tree:
- :labels
- milestones:
- :events
- issues:
- :events
- notes:
@ -39,9 +42,6 @@ project_tree:
- protected_branches:
- :merge_access_levels
- :push_access_levels
- :labels
- milestones:
- :events
- :project_feature
# Only include the following attributes for the models specified.
@ -73,5 +73,7 @@ excluded_attributes:
methods:
statuses:
- :type
services:
- :type
merge_request_diff:
- :utf8_st_diffs

View file

@ -61,11 +61,17 @@ module Gitlab
def restore_project
return @project unless @tree_hash
project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
@project.update(project_params)
@project
end
def project_params
@tree_hash.reject do |key, value|
# return params that are not 1 to many or 1 to 1 relations
value.is_a?(Array) || key == key.singularize
end
end
# Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+

View file

@ -17,19 +17,18 @@ module Gitlab
end
end
def generate
token = Devise.friendly_token(TOKEN_LENGTH)
def token
Gitlab::Redis.with do |redis|
redis.set(redis_key, token, ex: EXPIRY_TIME)
end
token = redis.get(redis_key)
token
end
if token
redis.expire(redis_key, EXPIRY_TIME)
else
token = Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME)
end
def value
Gitlab::Redis.with do |redis|
redis.get(redis_key)
token
end
end

View file

@ -73,8 +73,8 @@ describe UsersController do
end
context 'forked project' do
let!(:project) { create(:project) }
let!(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:project) { create(:project) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
sign_in(user)

View file

@ -62,6 +62,7 @@ describe 'Issue Boards', feature: true, js: true do
let(:bug) { create(:label, project: project, name: 'Bug') }
let!(:backlog) { create(:label, project: project, name: 'Backlog') }
let!(:done) { create(:label, project: project, name: 'Done') }
let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
@ -75,7 +76,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) }
let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project)
@ -441,6 +442,34 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_empty_boards((2..4))
end
it 'filters by label with space after reload' do
page.within '.issues-filters' do
click_button('Label')
wait_for_ajax
page.within '.dropdown-menu-labels' do
click_link(accepting.title)
wait_for_vue_resource(spinner: false)
find('.dropdown-menu-close').click
end
end
# Test after reload
page.evaluate_script 'window.location.reload()'
wait_for_vue_resource
page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end
it 'infinite scrolls list with label filter' do
50.times do
create(:labeled_issue, project: project, labels: [testing])

View file

@ -0,0 +1,15 @@
require 'spec_helper'
describe 'Dashboard snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
login_as(project.owner)
visit dashboard_snippets_path
end
it_behaves_like 'paginated snippets'
end
end

View file

@ -369,6 +369,24 @@ describe 'Issues', feature: true do
end
end
describe 'update labels from issue#show', js: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
let!(:label) { create(:label, project: project) }
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'will not send ajax request when no data is changed' do
page.within '.labels' do
click_link 'Edit'
first('.dropdown-menu-close').click
expect(page).not_to have_selector('.block-loading')
end
end
end
describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }

View file

@ -0,0 +1,14 @@
require 'spec_helper'
describe 'Project snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
visit namespace_project_snippets_path(project.namespace, project)
end
it_behaves_like 'paginated snippets'
end
end

View file

@ -0,0 +1,14 @@
require 'spec_helper'
describe 'Snippets', feature: true do
context 'when the project has snippets' do
let(:project) { create(:empty_project, :public) }
let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
before do
allow(Snippet).to receive(:default_per_page).and_return(1)
visit snippets_path(username: project.owner.username)
end
it_behaves_like 'paginated snippets'
end
end

View file

@ -11,7 +11,7 @@ describe 'Unsubscribe links', feature: true do
let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
let(:header_link) { mail.header['List-Unsubscribe'] }
let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets
let(:body_link) { body.find_link('unsubscribe')['href'] }
before do

View file

@ -3,30 +3,16 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
context 'when the user has snippets' do
let(:user) { create(:user) }
let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
before do
create_list(:snippet, 25, :public, author: user)
allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user)
page.within('.user-profile-nav') { click_link 'Snippets' }
wait_for_ajax
end
it 'is limited to 20 items per page' do
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20)
end
context 'clicking on the link to the second page' do
before do
click_link('2')
wait_for_ajax
end
it 'shows the remaining snippets' do
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5)
end
end
it_behaves_like 'paginated snippets', remote: true
end
end

View file

@ -11,7 +11,7 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:fork_project) { Projects::ForkService.new(project, user).execute }
it "returns false if there are no appropriate permissions" do

View file

@ -48,9 +48,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
$('.dropdow-content a').each((i, $link) => {
if (i < 5) {
$link.get(0).click();
$('.dropdown-content a').each(function (i) {
if (i < saveLabelCount) {
$(this).get(0).click();
}
});
@ -70,9 +70,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
$('.dropdow-content a').each((i, $link) => {
if (i < 5) {
$link.get(0).click();
$('.dropdown-content a').each(function (i) {
if (i < saveLabelCount) {
$(this).get(0).click();
}
});
@ -86,4 +86,3 @@
});
});
})();

View file

@ -64,7 +64,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes user lfs tokens' do
user = create(:user)
ip = 'ip'
token = Gitlab::LfsToken.new(user).generate
token = Gitlab::LfsToken.new(user).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
@ -73,7 +73,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
ip = 'ip'
token = Gitlab::LfsToken.new(key).generate
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))

View file

@ -2231,6 +2231,31 @@
],
"milestones": [
{
"id": 1,
"title": "test milestone",
"project_id": 8,
"description": "test milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
"state": "active",
"iid": 1,
"events": [
{
"id": 487,
"target_type": "Milestone",
"target_id": 1,
"title": null,
"data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
"action": 1,
"author_id": 18
}
]
},
{
"id": 20,
"title": "v4.0",
@ -6918,6 +6943,7 @@
"note_events": true,
"build_events": true,
"category": "issue_tracker",
"type": "CustomIssueTrackerService",
"default": true,
"wiki_page_events": true
},
@ -7372,5 +7398,16 @@
}
]
}
]
],
"project_feature": {
"builds_access_level": 0,
"created_at": "2014-12-26T09:26:45.000Z",
"id": 2,
"issues_access_level": 0,
"merge_requests_access_level": 20,
"project_id": 4,
"snippets_access_level": 20,
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
}
}

View file

@ -107,6 +107,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
it 'has a project feature' do
restored_project_json
expect(project.project_feature).not_to be_nil
end
it 'restores the correct service' do
restored_project_json
expect(CustomIssueTrackerService.first).not_to be_nil
end
context 'Merge requests' do
before do
restored_project_json

View file

@ -111,6 +111,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end
it 'saves the correct service type' do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end
it 'has project feature' do
project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty
@ -161,6 +165,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_id: ci_pipeline.sha)
create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)

View file

@ -1,10 +1,10 @@
require 'spec_helper'
describe Gitlab::LfsToken, lib: true do
describe '#generate and #value' do
describe '#token' do
shared_examples 'an LFS token generator' do
it 'returns a randomly generated token' do
token = handler.generate
token = handler.token
expect(token).not_to be_nil
expect(token).to be_a String
@ -12,9 +12,9 @@ describe Gitlab::LfsToken, lib: true do
end
it 'returns the correct token based on the key' do
token = handler.generate
token = handler.token
expect(handler.value).to eq(token)
expect(handler.token).to eq(token)
end
end

View file

@ -169,8 +169,9 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
it 'has a List-Unsubscribe header' do
it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }

View file

@ -34,6 +34,12 @@ describe CycleAnalytics::Summary, models: true do
expect(subject.commits).to eq(0)
end
it "finds a large (> 100) snumber of commits if present" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
expect(subject.commits).to eq(100)
end
end
describe "#deploys" do

View file

@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
let(:user) { create(:user, namespace: namespace) }
before do
create(:project_member, :reporter, user: user, project: project_from)
@project_to = fork_project(project_from, user)
end

View file

@ -25,5 +25,21 @@ describe CustomIssueTrackerService, models: true do
it { is_expected.not_to validate_presence_of(:issues_url) }
it { is_expected.not_to validate_presence_of(:new_issue_url) }
end
context 'title' do
let(:issue_tracker) { described_class.new(properties: {}) }
it 'sets a default title' do
issue_tracker.title = nil
expect(issue_tracker.title).to eq('Custom Issue Tracker')
end
it 'sets the custom title' do
issue_tracker.title = 'test title'
expect(issue_tracker.title).to eq('test title')
end
end
end
end

View file

@ -10,7 +10,8 @@ describe API::Helpers, api: true do
let(:key) { create(:key, user: user) }
let(:params) { {} }
let(:env) { {} }
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier)
clear_env
@ -52,17 +53,43 @@ describe API::Helpers, api: true do
describe ".current_user" do
subject { current_user }
describe "when authenticating via Warden" do
describe "Warden authentication" do
before { doorkeeper_guard_returns false }
context "fails" do
it { is_expected.to be_nil }
context "with invalid credentials" do
context "GET request" do
before { env['REQUEST_METHOD'] = 'GET' }
it { is_expected.to be_nil }
end
end
context "succeeds" do
context "with valid credentials" do
before { warden_authenticate_returns user }
it { is_expected.to eq(user) }
context "GET request" do
before { env['REQUEST_METHOD'] = 'GET' }
it { is_expected.to eq(user) }
end
context "HEAD request" do
before { env['REQUEST_METHOD'] = 'HEAD' }
it { is_expected.to eq(user) }
end
context "PUT request" do
before { env['REQUEST_METHOD'] = 'PUT' }
it { is_expected.to be_nil }
end
context "POST request" do
before { env['REQUEST_METHOD'] = 'POST' }
it { is_expected.to be_nil }
end
context "DELETE request" do
before { env['REQUEST_METHOD'] = 'DELETE' }
it { is_expected.to be_nil }
end
end
end

View file

@ -18,7 +18,7 @@ describe API::API, api: true do
end
let(:project_user2) do
create(:project_member, :guest, user: user2, project: project)
create(:project_member, :reporter, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do

View file

@ -111,7 +111,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end
@ -131,7 +131,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end
end

View file

@ -39,7 +39,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
it { expect(response).to have_http_status(403) }
it { expect(response).to have_http_status(401) }
end
end
@ -77,7 +77,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
it { expect(response).to have_http_status(403) }
it { expect(response).to have_http_status(401) }
end
end

View file

@ -257,6 +257,29 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file'
end
describe 'when using a user key' do
let(:authorization) { authorize_user_key }
context 'when user allowed' do
let(:update_permissions) do
project.team << [user, :master]
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'when user not allowed' do
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it 'responds with status 404' do
expect(response).to have_http_status(404)
end
end
end
context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
@ -1110,7 +1133,11 @@ describe 'Git LFS API and storage' do
end
def authorize_deploy_key
ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate)
ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
end
def authorize_user_key
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
end
def fork_project(project, user, object = nil)

View file

@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
description: 'wow such project')
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
@from_project.add_user(@to_user, :developer)
end
context 'fork project' do
context 'when forker is a guest' do
before do
@guest = create(:user)
@from_project.add_user(@guest, :guest)
end
subject { fork_project(@from_project, @guest) }
it { is_expected.not_to be_persisted }
it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
end
describe "successfully creates project in the user namespace" do
let(:to_project) { fork_project(@from_project, @to_user) }
it { expect(to_project).to be_persisted }
it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
expect(@existing_project.persisted?).to be_truthy
expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken'])
end
@ -81,18 +97,23 @@ describe Projects::ForkService, services: true do
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
expect(to_project).to be_persisted
expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
expect(to_project.star_count).to be_zero
expect(to_project.star_count).to be_zero
end
end

View file

@ -445,7 +445,7 @@ describe SystemNoteService, services: true do
end
context 'commit with cross-reference from fork' do
let(:author2) { create(:user) }
let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:forked_project) { Projects::ForkService.new(project, author2).execute }
let(:commit2) { forked_project.commit }

View file

@ -4,24 +4,28 @@ module CycleAnalyticsHelpers
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
def create_commit(message, project, user, branch_name)
filename = random_git_name
def create_commit(message, project, user, branch_name, count: 1)
oldrev = project.repository.commit(branch_name).sha
commit_shas = Array.new(count) do |index|
filename = random_git_name
options = {
committer: project.repository.user_to_committer(user),
author: project.repository.user_to_committer(user),
commit: { message: message, branch: branch_name, update_ref: true },
file: { content: "content", path: filename, update: false }
}
options = {
committer: project.repository.user_to_committer(user),
author: project.repository.user_to_committer(user),
commit: { message: message, branch: branch_name, update_ref: true },
file: { content: "content", path: filename, update: false }
}
commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
project.repository.commit(commit_sha)
commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
project.repository.commit(commit_sha)
commit_sha
end
GitPushService.new(project,
user,
oldrev: oldrev,
newrev: commit_sha,
newrev: commit_shas.last,
ref: 'refs/heads/master').execute
end

View file

@ -0,0 +1,18 @@
# These shared examples expect a `snippets` array of snippets
RSpec.shared_examples 'paginated snippets' do |remote: false|
it "is limited to #{Snippet.default_per_page} items per page" do
expect(page.all('.snippets-list-holder .snippet-row').count).to eq(Snippet.default_per_page)
end
context 'clicking on the link to the second page' do
before do
click_link('2')
wait_for_ajax if remote
end
it 'shows the remaining snippets' do
remaining_snippets_count = [snippets.size - Snippet.default_per_page, Snippet.default_per_page].min
expect(page).to have_selector('.snippets-list-holder .snippet-row', count: remaining_snippets_count)
end
end
end

View file

@ -0,0 +1,35 @@
require 'spec_helper'
describe 'ci/lints/show' do
include Devise::TestHelpers
before do
assign(:status, true)
assign(:stages, %w[test])
assign(:builds, builds)
end
context 'when builds attrbiutes contain HTML nodes' do
let(:builds) do
[ { name: 'rspec', stage: 'test', commands: '<h1>rspec</h1>' } ]
end
it 'does not render HTML elements' do
render
expect(rendered).not_to have_css('h1', text: 'rspec')
end
end
context 'when builds attributes do not contain HTML nodes' do
let(:builds) do
[ { name: 'rspec', stage: 'test', commands: 'rspec' } ]
end
it 'shows configuration in the table' do
render
expect(rendered).to have_css('td pre', text: 'rspec')
end
end
end