Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-03-24 12:09:42 +00:00
parent 6f7881ee9d
commit 729e3765d5
88 changed files with 1616 additions and 418 deletions

View file

@ -202,7 +202,6 @@ GitlabSecurity/PublicSend:
Gitlab/DuplicateSpecLocation:
Exclude:
- ee/spec/controllers/groups_controller_spec.rb
- ee/spec/controllers/projects/jobs_controller_spec.rb
- ee/spec/helpers/auth_helper_spec.rb
- ee/spec/lib/gitlab/gl_repository_spec.rb
@ -215,7 +214,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/services/merge_requests/refresh_service_spec.rb
- ee/spec/services/merge_requests/update_service_spec.rb
- ee/spec/services/system_hooks_service_spec.rb
- ee/spec/controllers/ee/groups_controller_spec.rb
- ee/spec/controllers/ee/projects/jobs_controller_spec.rb
- ee/spec/helpers/ee/auth_helper_spec.rb
- ee/spec/lib/ee/gitlab/gl_repository_spec.rb

View file

@ -1,5 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import discussionNavigation from '../mixins/discussion_navigation';
@ -18,13 +18,11 @@ export default {
'getNoteableData',
'resolvableDiscussionsCount',
'unresolvedDiscussionsCount',
'discussions',
]),
isLoggedIn() {
return this.getUserData.id;
},
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
allResolved() {
return this.unresolvedDiscussionsCount === 0;
},
@ -34,6 +32,21 @@ export default {
resolvedDiscussionsCount() {
return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
},
toggeableDiscussions() {
return this.discussions.filter(discussion => !discussion.individual_note);
},
allExpanded() {
return this.toggeableDiscussions.every(discussion => discussion.expanded);
},
},
methods: {
...mapActions(['setExpandDiscussions']),
handleExpandDiscussions() {
this.setExpandDiscussions({
discussionIds: this.toggeableDiscussions.map(discussion => discussion.id),
expanded: !this.allExpanded,
});
},
},
};
</script>
@ -44,8 +57,8 @@ export default {
ref="discussionCounter"
class="line-resolve-all-container full-width-mobile"
>
<div class="full-width-mobile d-flex d-sm-block">
<div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all">
<div class="full-width-mobile d-flex d-sm-flex">
<div class="line-resolve-all">
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
@ -75,7 +88,7 @@ export default {
<div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
<button
v-gl-tooltip
title="Jump to next unresolved thread"
:title="__('Jump to next unresolved thread')"
class="btn btn-default discussion-next-btn"
data-track-event="click_button"
data-track-label="mr_next_unresolved_thread"
@ -85,6 +98,16 @@ export default {
<icon name="comment-next" />
</button>
</div>
<div v-if="isLoggedIn" class="btn-group btn-group-sm" role="group">
<button
v-gl-tooltip
:title="__('Toggle all threads')"
class="btn btn-default toggle-all-discussions-btn"
@click="handleExpandDiscussions"
>
<icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
</button>
</div>
</div>
</div>
</template>

View file

@ -46,6 +46,10 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const setExpandDiscussions = ({ commit }, { discussionIds, expanded }) => {
commit(types.SET_EXPAND_DISCUSSIONS, { discussionIds, expanded });
};
export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => {
const config =
filter !== undefined
@ -54,6 +58,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
return axios.get(path, config).then(({ data }) => {
commit(types.SET_INITIAL_DISCUSSIONS, data);
dispatch('updateResolvableDiscussionsCounts');
});
};

View file

@ -24,6 +24,7 @@ export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS';
export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS';
export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID';

View file

@ -190,6 +190,15 @@ export default {
});
},
[types.SET_EXPAND_DISCUSSIONS](state, { discussionIds, expanded }) {
if (discussionIds?.length) {
discussionIds.forEach(discussionId => {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
Object.assign(discussion, { expanded });
});
}
},
[types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);

View file

@ -1,8 +1,9 @@
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import dateFormat from 'dateformat';
import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
@ -12,7 +13,7 @@ export default {
ClipboardButton,
ExpandButton,
GlLink,
Icon,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -24,17 +25,33 @@ export default {
},
},
computed: {
evidenceTitle() {
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName });
evidences() {
return this.release.evidences;
},
evidenceUrl() {
return this.release.assets && this.release.assets.evidenceFilePath;
},
methods: {
evidenceTitle(index) {
const [tag, evidence, filename] = this.release.evidences[index].filepath.split('/').slice(-3);
return sprintf(__('%{tag}-%{evidence}-%{filename}'), { tag, evidence, filename });
},
shortSha() {
return truncateSha(this.sha);
evidenceUrl(index) {
return this.release.evidences[index].filepath;
},
sha() {
return this.release.evidenceSha;
sha(index) {
return this.release.evidences[index].sha;
},
shortSha(index) {
return truncateSha(this.release.evidences[index].sha);
},
collectedAt(index) {
return dateFormat(this.release.evidences[index].collectedAt, 'mmmm dS, yyyy, h:MM TT');
},
timeSummary(index) {
const { format } = getTimeago();
const summary = sprintf(__(' Collected %{time}'), {
time: format(this.release.evidences[index].collectedAt),
});
return summary;
},
},
};
@ -43,34 +60,45 @@ export default {
<template>
<div>
<div class="card-text prepend-top-default">
<b>
{{ __('Evidence collection') }}
</b>
<b>{{ __('Evidence collection') }}</b>
</div>
<div class="d-flex align-items-baseline">
<gl-link
v-gl-tooltip
class="monospace"
:title="__('Download evidence JSON')"
:download="evidenceTitle"
:href="evidenceUrl"
>
<icon name="review-list" class="align-top append-right-4" /><span>{{ evidenceTitle }}</span>
</gl-link>
<div v-for="(evidence, index) in evidences" :key="evidenceTitle(index)" class="mb-2">
<div class="d-flex align-items-center">
<gl-link
v-gl-tooltip
class="d-flex align-items-center monospace"
:title="__('Download evidence JSON')"
:download="evidenceTitle(index)"
:href="evidenceUrl(index)"
>
<gl-icon name="review-list" class="align-middle append-right-8" />
<span>{{ evidenceTitle(index) }}</span>
</gl-link>
<expand-button>
<template slot="short">
<span class="js-short monospace">{{ shortSha }}</span>
</template>
<template slot="expanded">
<span class="js-expanded monospace gl-pl-1">{{ sha }}</span>
</template>
</expand-button>
<clipboard-button
:title="__('Copy evidence SHA')"
:text="sha"
css-class="btn-default btn-transparent btn-clipboard"
/>
<expand-button>
<template slot="short">
<span class="js-short monospace">{{ shortSha(index) }}</span>
</template>
<template slot="expanded">
<span class="js-expanded monospace gl-pl-1">{{ sha(index) }}</span>
</template>
</expand-button>
<clipboard-button
:title="__('Copy evidence SHA')"
:text="sha(index)"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div class="d-flex align-items-center text-muted">
<gl-icon
v-gl-tooltip
name="clock"
class="align-middle append-right-8"
:title="collectedAt(index)"
/>
<span>{{ timeSummary(index) }}</span>
</div>
</div>
</div>
</template>

View file

@ -44,7 +44,7 @@ export default {
return this.release.assets || {};
},
hasEvidence() {
return Boolean(this.release.evidenceSha);
return Boolean(this.release.evidences && this.release.evidences.length);
},
milestones() {
return this.release.milestones || [];

View file

@ -68,6 +68,23 @@
.header-user-avatar {
border-color: $search-and-nav-links;
}
.header-user-notification-dot {
border: 2px solid $nav-svg-color;
}
}
&:focus:hover,
&:focus {
&.header-user-dropdown-toggle .header-user-notification-dot {
border-color: $white-light;
}
}
&:hover {
&.header-user-dropdown-toggle .header-user-notification-dot {
border-color: $nav-svg-color + 33;
}
}
&:hover,

View file

@ -567,6 +567,14 @@
border: 1px solid $gray-normal;
}
.header-user-notification-dot {
background-color: $orange-500;
height: 10px;
width: 10px;
right: 8px;
top: -8px;
}
.with-performance-bar .navbar-gitlab {
top: $performance-bar-height;
}

View file

@ -842,11 +842,11 @@ $note-form-margin-left: 72px;
white-space: nowrap;
}
.btn-group {
margin-left: -4px;
.discussion-next-btn {
border-radius: 0;
}
.discussion-next-btn {
.toggle-all-discussions-btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
@ -859,7 +859,6 @@ $note-form-margin-left: 72px;
}
&.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0;
border-right: 0;
@ -873,6 +872,10 @@ $note-form-margin-left: 72px;
}
}
}
&.discussion-next-btn {
border-right: 0;
}
}
}
@ -884,12 +887,9 @@ $note-form-margin-left: 72px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
font-size: $gl-btn-small-font-size;
&.has-next-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
.line-resolve-btn {
margin-right: 5px;

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Projects
module Releases
class EvidencesController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :release
before_action :authorize_read_release_evidence!
def show
respond_to do |format|
format.json do
render json: evidence.summary
end
end
end
private
def authorize_read_release_evidence!
access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true)
access_denied! unless can?(current_user, :read_release_evidence, evidence)
end
def release
@release ||= project.releases.find_by_tag!(sanitized_tag_name)
end
def evidence
release.evidences.find(params[:id])
end
def sanitized_tag_name
CGI.unescape(params[:tag])
end
end
end
end

View file

@ -11,7 +11,6 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
end
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_read_release_evidence!, only: [:evidence]
def index
respond_to do |format|
@ -22,14 +21,6 @@ class Projects::ReleasesController < Projects::ApplicationController
end
end
def evidence
respond_to do |format|
format.json do
render json: release.evidence_summary
end
end
end
def show
return render_404 unless Feature.enabled?(:release_show_page, project, default_enabled: true)
@ -64,11 +55,6 @@ class Projects::ReleasesController < Projects::ApplicationController
access_denied! unless can?(current_user, :update_release, release)
end
def authorize_read_release_evidence!
access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true)
access_denied! unless can?(current_user, :read_release_evidence, release)
end
def release
@release ||= project.releases.find_by_tag!(sanitized_tag_name)
end

View file

@ -52,10 +52,17 @@ class EventsFinder
if current_user && scope == 'all'
EventCollection.new(current_user.authorized_projects).all_project_events
else
source.events
# EventCollection is responsible for applying the feature flag
apply_feature_flags(source.events)
end
end
def apply_feature_flags(events)
return events if ::Feature.enabled?(:wiki_events)
events.not_wiki_page
end
# rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events)
events.merge(Project.public_or_visible_to_user(current_user))

View file

@ -56,12 +56,17 @@ module Resolvers
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
# make sure it's loaded and not `nil` before continuing.
project = object.respond_to?(:sync) ? object.sync : object
return Issue.none if project.nil?
parent = object.respond_to?(:sync) ? object.sync : object
return Issue.none if parent.nil?
if parent.is_a?(Group)
args[:group_id] = parent.id
else
args[:project_id] = parent.id
end
# Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
args[:project_id] = project.id
args[:iids] ||= [args[:iid]].compact
args[:attempt_project_search_optimizations] = args[:search].present?

View file

@ -43,6 +43,12 @@ module Types
description: 'Parent group',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find }
field :issues,
Types::IssueType.connection_type,
null: true,
description: 'Issues of the group',
resolver: Resolvers::IssuesResolver
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Find milestones',
resolver: Resolvers::MilestoneResolver

View file

@ -65,6 +65,10 @@ module NavHelper
%w(groups#issues labels#index milestones#index boards#index boards#show)
end
def show_user_notification_dot?
experiment_enabled?(:ci_notification_dot)
end
private
def get_header_links

View file

@ -36,6 +36,8 @@ class Event < ApplicationRecord
expired: EXPIRED
).freeze
WIKI_ACTIONS = [CREATED, UPDATED, DESTROYED].freeze
TARGET_TYPES = HashWithIndifferentAccess.new(
issue: Issue,
milestone: Milestone,
@ -81,7 +83,10 @@ class Event < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
scope :merged, -> { where(action: MERGED) }
scope :for_wiki_page, -> { where(target_type: WikiPage::Meta.name) }
scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') }
# Needed to implement feature flag: can be removed when feature flag is removed
scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
@ -229,7 +234,7 @@ class Event < ApplicationRecord
end
def wiki_page?
target_type == WikiPage::Meta.name
target_type == 'WikiPage::Meta'
end
def milestone

View file

@ -33,16 +33,23 @@ class EventCollection
project_events
end
relation = apply_feature_flags(relation)
relation = paginate_events(relation)
relation.with_associations.to_a
end
def all_project_events
Event.from_union([project_events]).recent
apply_feature_flags(Event.from_union([project_events]).recent)
end
private
def apply_feature_flags(events)
return events if ::Feature.enabled?(:wiki_events)
events.not_wiki_page
end
def project_events
relation_with_join_lateral('project_id', projects)
end

View file

@ -78,8 +78,6 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
ignore_column :state, remove_with: '12.10', remove_after: '2020-03-22'
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?

View file

@ -261,8 +261,6 @@ class MergeRequest < ApplicationRecord
includes(:metrics)
end
ignore_column :state, remove_with: '12.10', remove_after: '2020-03-22'
after_save :keep_around_commit, unless: :importing?
alias_attribute :project, :target_project

View file

@ -16,7 +16,7 @@ class Release < ApplicationRecord
has_many :milestone_releases
has_many :milestones, through: :milestone_releases
has_one :evidence
has_many :evidences, inverse_of: :release, class_name: 'Releases::Evidence'
default_value_for :released_at, allows_nil: false do
Time.zone.now
@ -28,7 +28,7 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) }
scope :preloaded, -> { includes(project: :namespace) }
scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }
scope :with_project_and_namespace, -> { includes(project: :namespace) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
@ -66,27 +66,27 @@ class Release < ApplicationRecord
end
def upcoming_release?
released_at.present? && released_at > Time.zone.now
released_at.present? && released_at.to_i > Time.zone.now.to_i
end
def historical_release?
released_at.present? && released_at < created_at
released_at.present? && released_at.to_i < created_at.to_i
end
def name
self.read_attribute(:name) || tag
end
def milestone_titles
self.milestones.map {|m| m.title }.sort.join(", ")
end
def evidence_sha
evidence&.summary_sha
evidences.first&.summary_sha
end
def evidence_summary
evidence&.summary || {}
end
def milestone_titles
self.milestones.map {|m| m.title }.sort.join(", ")
evidences.first&.summary || {}
end
private

View file

@ -1,15 +1,17 @@
# frozen_string_literal: true
class Evidence < ApplicationRecord
class Releases::Evidence < ApplicationRecord
include ShaAttribute
include Presentable
belongs_to :release
belongs_to :release, inverse_of: :evidences
before_validation :generate_summary_and_sha
default_scope { order(created_at: :asc) }
sha_attribute :summary_sha
alias_attribute :collected_at, :created_at
def milestones
@milestones ||= release.milestones.includes(:issues)

View file

@ -2,31 +2,4 @@
class ReleasePolicy < BasePolicy
delegate { @subject.project }
rule { allowed_to_read_evidence & external_authorization_service_disabled }.policy do
enable :read_release_evidence
end
##
# evidence.summary includes the following entities:
# - Release
# - git-tag (Repository)
# - Project
# - Milestones
# - Issues
condition(:allowed_to_read_evidence) do
can?(:read_release) &&
can?(:download_code) &&
can?(:read_project) &&
can?(:read_milestone) &&
can?(:read_issue)
end
##
# Currently, we don't support release evidence for the GitLab instances
# that enables external authorization services.
# See https://gitlab.com/gitlab-org/gitlab/issues/121930.
condition(:external_authorization_service_disabled) do
!Gitlab::ExternalAuthorization::Config.enabled?
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Releases
class EvidencePolicy < BasePolicy
delegate { @subject.release.project }
rule { allowed_to_read_evidence & external_authorization_service_disabled }.policy do
enable :read_release_evidence
end
##
# evidence.summary includes the following entities:
# - Release
# - git-tag (Repository)
# - Project
# - Milestones
# - Issues
condition(:allowed_to_read_evidence) do
can?(:read_release) &&
can?(:download_code) &&
can?(:read_project) &&
can?(:read_milestone) &&
can?(:read_issue)
end
##
# Currently, we don't support release evidence for the GitLab instances
# that enables external authorization services.
# See https://gitlab.com/gitlab-org/gitlab/issues/121930.
condition(:external_authorization_service_disabled) do
!Gitlab::ExternalAuthorization::Config.enabled?
end
end
end

View file

@ -44,9 +44,10 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
end
def evidence_file_path
return unless release.evidence.present?
evidence = release.evidences.first
return unless evidence
evidence_project_release_url(project, release.to_param, format: :json)
project_evidence_url(project, release, evidence, format: :json)
end
private

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Releases
class EvidencePresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :evidence
def filepath
release = evidence.release
project = release.project
project_evidence_url(project, release, evidence, format: :json)
end
end
end

View file

@ -8,6 +8,8 @@
# EventCreateService.new.new_issue(issue, current_user)
#
class EventCreateService
IllegalActionError = Class.new(StandardError)
def open_issue(issue, current_user)
create_record_event(issue, current_user, Event::CREATED)
end
@ -80,6 +82,19 @@ class EventCreateService
create_push_event(BulkPushEventPayloadService, project, current_user, push_data)
end
# Create a new wiki page event
#
# @param [WikiPage::Meta] wiki_page_meta The event target
# @param [User] current_user The event author
# @param [Integer] action One of the Event::WIKI_ACTIONS
def wiki_event(wiki_page_meta, current_user, action)
return unless Feature.enabled?(:wiki_events)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
create_record_event(wiki_page_meta, current_user, action)
end
private
def create_record_event(record, current_user, status)

View file

@ -1,19 +1,61 @@
# frozen_string_literal: true
module WikiPages
# There are 3 notions of 'action' that inheriting classes must implement:
#
# - external_action: the action we report to external clients with webhooks
# - usage_counter_action: the action that we count in out internal counters
# - event_action: what we record as the value of `Event#action`
class BaseService < ::BaseService
private
def execute_hooks(page, action = 'create')
page_data = Gitlab::DataBuilder::WikiPage.build(page, current_user, action)
def execute_hooks(page)
page_data = payload(page)
@project.execute_hooks(page_data, :wiki_page_hooks)
@project.execute_services(page_data, :wiki_page_hooks)
increment_usage(action)
increment_usage
create_wiki_event(page)
end
# Passed to web-hooks, and send to external consumers.
def external_action
raise NotImplementedError
end
# Passed to the WikiPageCounter to count events.
# Must be one of WikiPageCounter::KNOWN_EVENTS
def usage_counter_action
raise NotImplementedError
end
# Used to create `Event` records.
# Must be a valid value for `Event#action`
def event_action
raise NotImplementedError
end
def payload(page)
Gitlab::DataBuilder::WikiPage.build(page, current_user, external_action)
end
# This method throws an error if the action is an unanticipated value.
def increment_usage(action)
Gitlab::UsageDataCounters::WikiPageCounter.count(action)
def increment_usage
Gitlab::UsageDataCounters::WikiPageCounter.count(usage_counter_action)
end
def create_wiki_event(page)
return unless ::Feature.enabled?(:wiki_events)
slug = slug_for_page(page)
Event.transaction do
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
EventCreateService.new.wiki_event(wiki_page_meta, current_user, event_action)
end
end
def slug_for_page(page)
page.slug
end
end
end

View file

@ -7,10 +7,22 @@ module WikiPages
page = WikiPage.new(project_wiki)
if page.create(@params)
execute_hooks(page, 'create')
execute_hooks(page)
end
page
end
def usage_counter_action
:create
end
def external_action
'create'
end
def event_action
Event::CREATED
end
end
end

View file

@ -4,10 +4,22 @@ module WikiPages
class DestroyService < WikiPages::BaseService
def execute(page)
if page&.delete
execute_hooks(page, 'delete')
execute_hooks(page)
end
page
end
def usage_counter_action
:delete
end
def external_action
'delete'
end
def event_action
Event::DESTROYED
end
end
end

View file

@ -3,11 +3,30 @@
module WikiPages
class UpdateService < WikiPages::BaseService
def execute(page)
# this class is not thread safe!
@old_slug = page.slug
if page.update(@params)
execute_hooks(page, 'update')
execute_hooks(page)
end
page
end
def usage_counter_action
:update
end
def external_action
'update'
end
def event_action
Event::UPDATED
end
def slug_for_page(page)
@old_slug.presence || super
end
end
end

View file

@ -68,6 +68,8 @@
%li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
- if show_user_notification_dot?
%span.header-user-notification-dot.rounded-circle.position-relative
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'

View file

@ -10,6 +10,6 @@ class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
release = Release.find_by_id(release_id)
return unless release
Evidence.create!(release: release)
Releases::Evidence.create!(release: release)
end
end

View file

@ -0,0 +1,5 @@
---
title: Add issues to graphQL group endpoint
merge_request: 27789
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Support multiple Evidences for a Release
merge_request: 26509
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Cache ES enabled namespaces and projects
merge_request: 27348
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Expose created_at property in Groups API
merge_request: 27824
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add toggle all discussions button to MRs
merge_request: 24670
author: Martin Hobert & Diego Louzán
type: added

View file

@ -0,0 +1,5 @@
---
title: "Run SAST using awk to pass env variables directly to docker without creating .env file"
merge_request: 21174
author: Florian Gaultier
type: fixed

View file

@ -170,8 +170,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :releases, only: [:index, :show, :edit], param: :tag, constraints: { tag: %r{[^/]+} } do
member do
get :evidence
get :downloads, path: 'downloads/*filepath', format: false
scope module: :releases do
resources :evidences, only: [:show]
end
end
end

View file

@ -3219,6 +3219,106 @@ type Group {
"""
id: ID!
"""
Issues of the group
"""
issues(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
ID of a user assigned to the issues, "none" and "any" values supported
"""
assigneeId: String
"""
Username of a user assigned to the issues
"""
assigneeUsername: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Issues closed after this date
"""
closedAfter: Time
"""
Issues closed before this date
"""
closedBefore: Time
"""
Issues created after this date
"""
createdAfter: Time
"""
Issues created before this date
"""
createdBefore: Time
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
IID of the issue. For example, "1"
"""
iid: String
"""
List of IIDs of issues. For example, [1, 2]
"""
iids: [String!]
"""
Labels applied to this issue
"""
labelName: [String]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Milestones applied to this issue
"""
milestoneTitle: [String]
"""
Search query for finding issues by title or description
"""
search: String
"""
Sort issues by this criteria
"""
sort: IssueSort = created_desc
"""
Current state of this issue
"""
state: IssuableState
"""
Issues updated after this date
"""
updatedAfter: Time
"""
Issues updated before this date
"""
updatedBefore: Time
): IssueConnection
"""
Indicates if Large File Storage (LFS) is enabled for namespace
"""

View file

@ -9242,6 +9242,225 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issues",
"description": "Issues of the group",
"args": [
{
"name": "iid",
"description": "IID of the issue. For example, \"1\"",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iids",
"description": "List of IIDs of issues. For example, [1, 2]",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "Current state of this issue",
"type": {
"kind": "ENUM",
"name": "IssuableState",
"ofType": null
},
"defaultValue": null
},
{
"name": "labelName",
"description": "Labels applied to this issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Milestones applied to this issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "createdBefore",
"description": "Issues created before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "createdAfter",
"description": "Issues created after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "updatedBefore",
"description": "Issues updated before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "updatedAfter",
"description": "Issues updated after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "closedBefore",
"description": "Issues closed before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "closedAfter",
"description": "Issues closed after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "search",
"description": "Search query for finding issues by title or description",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sort",
"description": "Sort issues by this criteria",
"type": {
"kind": "ENUM",
"name": "IssueSort",
"ofType": null
},
"defaultValue": "created_desc"
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "IssueConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lfsEnabled",
"description": "Indicates if Large File Storage (LFS) is enabled for namespace",

View file

@ -49,7 +49,8 @@ GET /groups
"full_name": "Foobar Group",
"full_path": "foo-bar",
"file_template_project_id": 1,
"parent_id": null
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z"
}
]
```
@ -85,6 +86,7 @@ GET /groups?statistics=true
"full_path": "foo-bar",
"file_template_project_id": 1,
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z",
"statistics": {
"storage_size" : 212,
"repository_size" : 33,
@ -157,7 +159,8 @@ GET /groups/:id/subgroups
"full_name": "Foobar Group",
"full_path": "foo-bar",
"file_template_project_id": 1,
"parent_id": 123
"parent_id": 123,
"created_at": "2020-01-15T12:36:29.590Z"
}
]
```
@ -282,6 +285,7 @@ Example response:
"runners_token": "ba324ca7b1c77fc20bb9",
"file_template_project_id": 1,
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z",
"projects": [
{
"id": 7,
@ -591,6 +595,7 @@ Example response:
"full_path": "foo-bar",
"file_template_project_id": 1,
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z",
"projects": [
{
"id": 9,

View file

@ -426,6 +426,15 @@ There are several rake tasks available to you via the command line:
- Performs an Elasticsearch import that indexes the snippets data.
- [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- Displays which projects are not indexed.
- [`sudo gitlab-rake gitlab:elastic:reindex_to_another_cluster[<SOURCE_CLUSTER_URL>,<DESTINATION_CLUSTER_URL>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- Creates a new index in the destination cluster and triggers a [reindex from
remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote)
such that the index is fully copied from the source index. This can be
useful when you wish to perform a migration to a new cluster as this
reindexing should be quicker than reindexing via GitLab. Note that remote
reindex requires your source cluster to be whitelisted in your destination
cluster in Elasticsearch settings as per [the
documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote).
### Environment Variables

View file

@ -9,6 +9,7 @@ module API
expose :created_at
expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :wiki_page, using: Entities::WikiPageBasic, if: ->(event, _options) { event.wiki_page? }
expose :push_event_payload,
as: :push_data,

View file

@ -19,6 +19,7 @@ module API
end
expose :request_access_enabled
expose :full_name, :full_path
expose :created_at
expose :parent_id
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes

View file

@ -22,6 +22,7 @@ module API
expose :commit_path, expose_nil: false
expose :tag_path, expose_nil: false
expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :assets do
expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources]
@ -33,6 +34,7 @@ module API
end
expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
end
expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :_links do
expose :self_url, as: :self, expose_nil: false
expose :merge_requests_url, expose_nil: false

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
module Releases
class Evidence < Grape::Entity
include ::API::Helpers::Presentable
expose :summary_sha, as: :sha
expose :filepath
expose :collected_at
end
end
end
end

View file

@ -4,7 +4,7 @@ module API
module Helpers
##
# This module makes it possible to use `app/presenters` with
# Grape Entities. It instantiates model presenter and passes
# Grape Entities. It instantiates the model presenter and passes
# options defined in the API endpoint to the presenter itself.
#
# present object, with: Entities::Something,
@ -22,6 +22,7 @@ module API
extend ActiveSupport::Concern
def initialize(object, options = {})
options = options.opts_hash if options.is_a?(Grape::Entity::Options)
super(object.present(options), options)
end
end

View file

@ -9,6 +9,7 @@ class EventFilter
ISSUE = 'issue'
COMMENTS = 'comments'
TEAM = 'team'
WIKI = 'wiki'
def initialize(filter)
# Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
@ -22,6 +23,8 @@ class EventFilter
# rubocop: disable CodeReuse/ActiveRecord
def apply_filter(events)
events = apply_feature_flags(events)
case filter
when PUSH
events.where(action: Event::PUSHED)
@ -33,6 +36,8 @@ class EventFilter
events.where(action: [Event::JOINED, Event::LEFT, Event::EXPIRED])
when ISSUE
events.where(action: [Event::CREATED, Event::UPDATED, Event::CLOSED, Event::REOPENED], target_type: 'Issue')
when WIKI
wiki_events(events)
else
events
end
@ -41,8 +46,20 @@ class EventFilter
private
def apply_feature_flags(events)
return events.not_wiki_page unless Feature.enabled?(:wiki_events)
events
end
def wiki_events(events)
return events unless Feature.enabled?(:wiki_events)
events.for_wiki_page
end
def filters
[ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM]
[ALL, PUSH, MERGED, ISSUE, COMMENTS, TEAM, WIKI]
end
end

View file

@ -36,9 +36,9 @@ sast:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
- ENVS=`printenv | grep -vE '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '`
- |
docker run $ENVS \
docker run \
$(awk 'BEGIN{for(v in ENVIRON) print v}' | grep -v -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | awk '{printf " -e %s", $0}') \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code

View file

@ -28,6 +28,12 @@ module Gitlab
environment: ::Gitlab.dev_env_or_com?,
enabled_ratio: 0.1,
tracking_category: 'Growth::Expansion::Experiment::SuggestPipeline'
},
ci_notification_dot: {
feature_toggle: :ci_notification_dot,
environment: ::Gitlab.dev_env_or_com?,
enabled_ratio: 0.1,
tracking_category: 'Growth::Expansion::Experiment::CiNotificationDot'
}
}.freeze

View file

@ -22,6 +22,9 @@ msgstr ""
msgid " (from %{timeoutSource})"
msgstr ""
msgid " Collected %{time}"
msgstr ""
msgid " Please sign in."
msgstr ""
@ -475,7 +478,7 @@ msgstr ""
msgid "%{tags} tags per image name"
msgstr ""
msgid "%{tag}-evidence.json"
msgid "%{tag}-%{evidence}-%{filename}"
msgstr ""
msgid "%{template_project_id} is unknown or invalid"
@ -21006,6 +21009,9 @@ msgstr ""
msgid "Toggle Sidebar"
msgstr ""
msgid "Toggle all threads"
msgstr ""
msgid "Toggle backtrace"
msgstr ""

View file

@ -72,15 +72,17 @@ end
# Define suffix in review app URL based on project
#
def slug
case ENV["CI_PROJECT_NAME"]
when 'gitlab-foss'
case ENV["CI_PROJECT_PATH"]
when 'gitlab-org/gitlab-foss'
'ce'
when 'gitlab'
when 'gitlab-org/gitlab'
'ee'
when 'gitlab-runner'
when 'gitlab-org/gitlab-runner'
'runner'
when 'omnibus-gitlab'
when 'gitlab-org/omnibus-gitlab'
'omnibus'
when 'gitlab-org/charts/gitlab'
'charts'
end
end

View file

@ -0,0 +1,165 @@
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Releases::EvidencesController do
let!(:project) { create(:project, :repository, :public) }
let_it_be(:private_project) { create(:project, :repository, :private) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let(:user) { developer }
before do
project.add_developer(developer)
project.add_reporter(reporter)
end
shared_examples_for 'successful request' do
it 'renders a 200' do
subject
expect(response).to have_gitlab_http_status(:success)
end
end
shared_examples_for 'not found' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET #show' do
let_it_be(:tag_name) { "v1.1.0-evidence" }
let!(:release) { create(:release, :with_evidence, project: project, tag: tag_name) }
let(:evidence) { release.evidences.first }
let(:tag) { CGI.escape(release.tag) }
let(:format) { :json }
subject do
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
tag: tag,
id: evidence.id,
format: format
}
end
before do
sign_in(user)
end
context 'when the user is a developer' do
it 'returns the correct evidence summary as a json' do
subject
expect(json_response).to eq(evidence.summary)
end
context 'when the release was created before evidence existed' do
before do
evidence.destroy
end
it_behaves_like 'not found'
end
end
context 'when the user is a guest for the project' do
before do
project.add_guest(user)
end
context 'when the project is private' do
let(:project) { private_project }
it_behaves_like 'not found'
end
context 'when the project is public' do
it_behaves_like 'successful request'
end
end
context 'when release is associated to a milestone which includes an issue' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:milestone) { create(:milestone, project: project, issues: [issue]) }
let_it_be(:release) { create(:release, project: project, tag: tag_name, milestones: [milestone]) }
before do
create(:evidence, release: release)
end
shared_examples_for 'does not show the issue in evidence' do
it do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['release']['milestones']
.all? { |milestone| milestone['issues'].nil? }).to eq(true)
end
end
shared_examples_for 'evidence not found' do
it do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples_for 'safely expose evidence' do
it_behaves_like 'does not show the issue in evidence'
context 'when the issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when the user is the author of the confidential issue' do
let(:issue) { create(:issue, :confidential, project: project, author: user) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when project is private' do
let!(:project) { create(:project, :repository, :private) }
it_behaves_like 'evidence not found'
end
context 'when project restricts the visibility of issues to project members only' do
let!(:project) { create(:project, :repository, :issues_private) }
it_behaves_like 'evidence not found'
end
end
context 'when user is non-project member' do
let(:user) { create(:user) }
it_behaves_like 'safely expose evidence'
end
context 'when user is auditor', if: Gitlab.ee? do
let(:user) { create(:user, :auditor) }
it_behaves_like 'safely expose evidence'
end
context 'when external authorization control is enabled' do
let(:user) { create(:user) }
before do
stub_application_setting(external_authorization_service_enabled: true)
end
it_behaves_like 'evidence not found'
end
end
end
end

View file

@ -3,11 +3,11 @@
require 'spec_helper'
describe Projects::ReleasesController do
let!(:project) { create(:project, :repository, :public) }
let!(:private_project) { create(:project, :repository, :private) }
let(:user) { developer }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let!(:project) { create(:project, :repository, :public) }
let_it_be(:private_project) { create(:project, :repository, :private) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:user) { developer }
let!(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) }
let!(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) }
@ -295,141 +295,6 @@ describe Projects::ReleasesController do
end
end
describe 'GET #evidence' do
let_it_be(:tag_name) { "v1.1.0-evidence" }
let!(:release) { create(:release, :with_evidence, project: project, tag: tag_name) }
let(:tag) { CGI.escape(release.tag) }
let(:format) { :json }
subject do
get :evidence, params: {
namespace_id: project.namespace,
project_id: project,
tag: tag,
format: format
}
end
before do
sign_in(user)
end
context 'when the user is a developer' do
it 'returns the correct evidence summary as a json' do
subject
expect(json_response).to eq(release.evidence.summary)
end
context 'when the release was created before evidence existed' do
before do
release.evidence.destroy
end
it 'returns an empty json' do
subject
expect(json_response).to eq({})
end
end
end
context 'when the user is a guest for the project' do
before do
project.add_guest(user)
end
context 'when the project is private' do
let(:project) { private_project }
it_behaves_like 'not found'
end
context 'when the project is public' do
it_behaves_like 'successful request'
end
end
context 'when release is associated to a milestone which includes an issue' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:milestone) { create(:milestone, project: project, issues: [issue]) }
let_it_be(:release) { create(:release, project: project, tag: tag_name, milestones: [milestone]) }
before do
create(:evidence, release: release)
end
shared_examples_for 'does not show the issue in evidence' do
it do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['release']['milestones']
.all? { |milestone| milestone['issues'].nil? }).to eq(true)
end
end
shared_examples_for 'evidence not found' do
it do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples_for 'safely expose evidence' do
it_behaves_like 'does not show the issue in evidence'
context 'when the issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when the user is the author of the confidential issue' do
let(:issue) { create(:issue, :confidential, project: project, author: user) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when project is private' do
let!(:project) { create(:project, :repository, :private) }
it_behaves_like 'evidence not found'
end
context 'when project restricts the visibility of issues to project members only' do
let!(:project) { create(:project, :repository, :issues_private) }
it_behaves_like 'evidence not found'
end
end
context 'when user is non-project member' do
let(:user) { create(:user) }
it_behaves_like 'safely expose evidence'
end
context 'when user is auditor', if: Gitlab.ee? do
let(:user) { create(:user, :auditor) }
it_behaves_like 'safely expose evidence'
end
context 'when external authorization control is enabled' do
let(:user) { create(:user) }
before do
stub_application_setting(external_authorization_service_enabled: true)
end
it_behaves_like 'evidence not found'
end
end
end
private
def get_index

View file

@ -25,12 +25,12 @@ FactoryBot.define do
factory :wiki_page_event do
action { Event::CREATED }
project { @overrides[:wiki_page]&.project || create(:project, :wiki_repo) }
target { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
transient do
wiki_page { create(:wiki_page, project: project) }
end
target { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
end
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :evidence do
factory :evidence, class: 'Releases::Evidence' do
release
end
end

View file

@ -3,7 +3,7 @@
require 'spec_helper'
describe EventsFinder do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:project1) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
@ -20,7 +20,7 @@ describe EventsFinder do
let(:opened_merge_request3) { create(:merge_request, source_project: project1, author: other_user) }
let!(:other_developer_event) { create(:event, project: project1, author: other_user, target: opened_merge_request3, action: Event::CREATED) }
let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let_it_be(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
@ -59,6 +59,32 @@ describe EventsFinder do
end
end
describe 'wiki events feature flag' do
let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) }
subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) }
context 'the wiki_events feature flag is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'omits the wiki page events' do
expect(finder.execute).to be_empty
end
end
context 'the wiki_events feature flag is enabled' do
before do
stub_feature_flags(wiki_events: true)
end
it 'can find the wiki events' do
expect(finder.execute).to match_array(events)
end
end
end
context 'dashboard events' do
before do
project1.add_developer(other_user)

View file

@ -22,6 +22,10 @@
"commit_path": { "type": "string" },
"tag_path": { "type": "string" },
"name": { "type": "string" },
"evidences": {
"type": "array",
"items": { "$ref": "release/evidence.json" }
},
"assets": {
"required": ["count", "links", "sources"],
"properties": {

View file

@ -0,0 +1,14 @@
{
"type": "object",
"required" : [
"sha",
"filepath",
"collected_at"
],
"properties" : {
"sha": { "type": "string" },
"filepath": { "type": "string" },
"collected_at": { "type": "date" }
},
"additionalProperties": false
}

View file

@ -1,11 +1,9 @@
import { createDateTimeFormat, languageCode } from '~/locale';
import { setLanguage } from '../helpers/locale_helper';
import { setLanguage } from 'helpers/locale_helper';
describe('locale', () => {
afterEach(() => {
setLanguage(null);
});
afterEach(() => setLanguage(null));
describe('languageCode', () => {
it('parses the lang attribute', () => {
@ -22,14 +20,12 @@ describe('locale', () => {
});
describe('createDateTimeFormat', () => {
beforeEach(() => {
setLanguage('de');
});
beforeEach(() => setLanguage('en'));
it('creates an instance of Intl.DateTimeFormat', () => {
const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
expect(dateFormat.format(new Date(2015, 6, 3))).toBe('3. Juli 2015');
expect(dateFormat.format(new Date(2015, 6, 3))).toBe('July 3, 2015');
});
});
});

View file

@ -75,17 +75,66 @@ describe('DiscussionCounter component', () => {
});
it.each`
title | resolved | hasNextBtn | isActive | icon | groupLength
${'hasNextButton'} | ${false} | ${true} | ${false} | ${'check-circle'} | ${2}
${'allResolved'} | ${true} | ${false} | ${true} | ${'check-circle-filled'} | ${0}
`('renders correctly if $title', ({ resolved, hasNextBtn, isActive, icon, groupLength }) => {
title | resolved | isActive | icon | groupLength
${'not allResolved'} | ${false} | ${false} | ${'check-circle'} | ${3}
${'allResolved'} | ${true} | ${true} | ${'check-circle-filled'} | ${1}
`('renders correctly if $title', ({ resolved, isActive, icon, groupLength }) => {
updateStore({ resolvable: true, resolved });
wrapper = shallowMount(DiscussionCounter, { store, localVue });
expect(wrapper.find(`.has-next-btn`).exists()).toBe(hasNextBtn);
expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
expect(wrapper.find({ name: icon }).exists()).toBe(true);
expect(wrapper.findAll('[role="group"').length).toBe(groupLength);
});
});
describe('toggle all threads button', () => {
let toggleAllButton;
const updateStoreWithExpanded = expanded => {
const discussion = { ...discussionMock, expanded };
store.commit(types.SET_INITIAL_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
};
afterEach(() => wrapper.destroy());
it('calls button handler when clicked', () => {
updateStoreWithExpanded(true);
wrapper.setMethods({ handleExpandDiscussions: jest.fn() });
toggleAllButton.trigger('click');
expect(wrapper.vm.handleExpandDiscussions).toHaveBeenCalledTimes(1);
});
it('collapses all discussions if expanded', () => {
updateStoreWithExpanded(true);
expect(wrapper.vm.allExpanded).toBe(true);
expect(toggleAllButton.find({ name: 'angle-up' }).exists()).toBe(true);
toggleAllButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.allExpanded).toBe(false);
expect(toggleAllButton.find({ name: 'angle-down' }).exists()).toBe(true);
});
});
it('expands all discussions if collapsed', () => {
updateStoreWithExpanded(false);
expect(wrapper.vm.allExpanded).toBe(false);
expect(toggleAllButton.find({ name: 'angle-down' }).exists()).toBe(true);
toggleAllButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.allExpanded).toBe(true);
expect(toggleAllButton.find({ name: 'angle-up' }).exists()).toBe(true);
});
});
});
});

View file

@ -329,6 +329,52 @@ describe('Notes Store mutations', () => {
});
});
describe('SET_EXPAND_DISCUSSIONS', () => {
it('should succeed when discussions are null', () => {
const state = {};
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds: null, expanded: true });
expect(state).toEqual({});
});
it('should succeed when discussions are empty', () => {
const state = {};
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds: [], expanded: true });
expect(state).toEqual({});
});
it('should open all closed discussions', () => {
const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false });
const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true });
const discussionIds = [discussion1.id, discussion2.id];
const state = { discussions: [discussion1, discussion2] };
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds, expanded: true });
state.discussions.forEach(discussion => {
expect(discussion.expanded).toEqual(true);
});
});
it('should close all opened discussions', () => {
const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false });
const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true });
const discussionIds = [discussion1.id, discussion2.id];
const state = { discussions: [discussion1, discussion2] };
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds, expanded: false });
state.discussions.forEach(discussion => {
expect(discussion.expanded).toEqual(false);
});
});
});
describe('UPDATE_NOTE', () => {
it('should update a note', () => {
const state = {

View file

@ -1,7 +1,6 @@
import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { GlLink, GlIcon } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@ -32,11 +31,11 @@ describe('Evidence Block', () => {
});
it('renders the evidence icon', () => {
expect(wrapper.find(Icon).props('name')).toBe('review-list');
expect(wrapper.find(GlIcon).props('name')).toBe('review-list');
});
it('renders the title for the dowload link', () => {
expect(wrapper.find(GlLink).text()).toBe(`${release.tagName}-evidence.json`);
expect(wrapper.find(GlLink).text()).toBe('v1.1.2-evidences-1.json');
});
it('renders the correct hover text for the download', () => {
@ -44,19 +43,19 @@ describe('Evidence Block', () => {
});
it('renders the correct file link for download', () => {
expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tagName}-evidence.json`);
expect(wrapper.find(GlLink).attributes().download).toBe('v1.1.2-evidences-1.json');
});
describe('sha text', () => {
it('renders the short sha initially', () => {
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidenceSha));
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidences[0].sha));
});
it('renders the long sha after expansion', () => {
wrapper.find('.js-text-expander-prepend').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-expanded').text()).toBe(release.evidenceSha);
expect(wrapper.find('.js-expanded').text()).toBe(release.evidences[0].sha);
});
});
});
@ -72,7 +71,7 @@ describe('Evidence Block', () => {
it('copies the sha', () => {
expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe(
release.evidenceSha,
release.evidences[0].sha,
);
});
});

View file

@ -43,7 +43,6 @@ export const release = {
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z',
released_at: '2019-08-26T17:54:04.807Z',
evidence_sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
author: {
id: 1,
name: 'Administrator',
@ -69,10 +68,28 @@ export const release = {
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false,
milestones,
evidences: [
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/1.json',
sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/2.json',
sha: '6ebd17a66e6a861175735416e49cf677678029805712dd71bb805c609e2d9108',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/3.json',
sha: '2f65beaf275c3cb4b4e24fb01d481cc475d69c957830833f15338384816b5cba',
collected_at: '2018-10-19 15:43:20 +0200',
},
],
assets: {
count: 5,
evidence_file_path:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidence.json',
sources: [
{
format: 'zip',

View file

@ -7,15 +7,20 @@ describe Resolvers::IssuesResolver do
let(:current_user) { create(:user) }
context "with a project" do
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:assignee) { create(:user) }
let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) }
let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:other_project) { create(:project, group: group) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:assignee) { create(:user) }
let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) }
let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) }
let_it_be(:issue3) { create(:issue, project: other_project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) }
let_it_be(:issue4) { create(:issue) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
context "with a project" do
before do
project.add_developer(current_user)
create(:label_link, label: label1, target: issue1)
@ -184,6 +189,20 @@ describe Resolvers::IssuesResolver do
end
end
context "with a group" do
before do
group.add_developer(current_user)
end
describe '#resolve' do
it 'finds all group issues' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user })
expect(result).to contain_exactly(issue1, issue2, issue3)
end
end
end
context "when passing a non existent, batch loaded project" do
let(:project) do
BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _|

View file

@ -117,4 +117,24 @@ describe NavHelper, :do_not_mock_admin_mode do
it { is_expected.to all(be_a(String)) }
end
describe '.show_user_notification_dot?' do
subject { helper.show_user_notification_dot? }
context 'when experiment is disabled' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(false)
end
it { is_expected.to be_falsey }
end
context 'when experiment is enabled' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(true)
end
it { is_expected.to be_truthy }
end
end
end

View file

@ -4,26 +4,29 @@ require 'spec_helper'
describe API::Entities::Release do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:entity) { described_class.new(release, current_user: user) }
describe 'evidence' do
let(:release) { create(:release, :with_evidence, project: project) }
subject { entity.as_json }
let_it_be(:release) { create(:release, :with_evidence, project: project) }
let(:evidence) { release.evidences.first }
let(:user) { create(:user) }
let(:entity) { described_class.new(release, current_user: user).as_json }
describe 'evidences' do
context 'when the current user can download code' do
let(:entity_evidence) { entity[:evidences].first }
it 'exposes the evidence sha and the json path' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :download_code, project).and_return(true)
expect(subject[:evidence_sha]).to eq(release.evidence_sha)
expect(subject[:assets][:evidence_file_path]).to eq(
Gitlab::Routing.url_helpers.evidence_project_release_url(project,
release.tag,
format: :json)
)
expect(entity_evidence[:sha]).to eq(evidence.summary_sha)
expect(entity_evidence[:collected_at]).to eq(evidence.collected_at)
expect(entity_evidence[:filepath]).to eq(
Gitlab::Routing.url_helpers.namespace_project_evidence_url(
namespace_id: project.namespace,
project_id: project,
tag: release,
id: evidence.id,
format: :json))
end
end
@ -33,8 +36,7 @@ describe API::Entities::Release do
allow(Ability).to receive(:allowed?)
.with(user, :download_code, project).and_return(false)
expect(subject.keys).not_to include(:evidence_sha)
expect(subject[:assets].keys).not_to include(:evidence_file_path)
expect(entity.keys).not_to include(:evidences)
end
end
end
@ -45,7 +47,7 @@ describe API::Entities::Release do
let(:issue_title) { 'title="%s"' % issue.title }
let(:release) { create(:release, project: project, description: "Now shipping #{issue.to_reference}") }
subject(:description_html) { entity.as_json[:description_html] }
subject(:description_html) { entity.as_json['description_html'] }
it 'renders special references if current user has access' do
project.add_reporter(user)

View file

@ -28,6 +28,8 @@ describe EventFilter do
let_it_be(:comments_event) { create(:event, :commented, project: public_project, target: public_project) }
let_it_be(:joined_event) { create(:event, :joined, project: public_project, target: public_project) }
let_it_be(:left_event) { create(:event, :left, project: public_project, target: public_project) }
let_it_be(:wiki_page_event) { create(:wiki_page_event) }
let_it_be(:wiki_page_update_event) { create(:wiki_page_event, :updated) }
let(:filtered_events) { described_class.new(filter).apply_filter(Event.all) }
@ -77,6 +79,34 @@ describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).to eq(Event.not_wiki_page)
end
end
end
context 'with the "wiki" filter' do
let(:filter) { described_class::WIKI }
it 'returns only wiki page events' do
expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).not_to include(wiki_page_event, wiki_page_update_event)
end
end
end
context 'with an unknown filter' do
@ -85,6 +115,16 @@ describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).to eq(Event.not_wiki_page)
end
end
end
context 'with a nil filter' do
@ -93,6 +133,16 @@ describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).to eq(Event.not_wiki_page)
end
end
end
end

View file

@ -94,7 +94,7 @@ releases:
- links
- milestone_releases
- milestones
- evidence
- evidences
links:
- release
project_members:

View file

@ -134,7 +134,7 @@ Release:
- created_at
- updated_at
- released_at
Evidence:
Releases::Evidence:
- id
- summary
- created_at

View file

@ -8,22 +8,68 @@ describe EventCollection do
let_it_be(:project) { create(:project_empty_repo, group: group) }
let_it_be(:projects) { Project.where(id: project.id) }
let_it_be(:user) { create(:user) }
let_it_be(:merge_request) { create(:merge_request) }
context 'with project events' do
before do
20.times do
event = create(:push_event, project: project, author: user)
create(:push_event_payload, event: event)
let_it_be(:push_event_payloads) do
Array.new(9) do
create(:push_event_payload,
event: create(:push_event, project: project, author: user))
end
create(:closed_issue_event, project: project, author: user)
end
it 'returns an Array of events' do
let_it_be(:merge_request_events) { create_list(:event, 10, :commented, project: project, target: merge_request) }
let_it_be(:closed_issue_event) { create(:closed_issue_event, project: project, author: user) }
let_it_be(:wiki_page_event) { create(:wiki_page_event, project: project) }
let(:push_events) { push_event_payloads.map(&:event) }
it 'returns an Array of events', :aggregate_failures do
most_recent_20_events = [
wiki_page_event,
closed_issue_event,
*push_events,
*merge_request_events
].sort_by(&:id).reverse.take(20)
events = described_class.new(projects).to_a
expect(events).to be_an_instance_of(Array)
expect(events).to match_array(most_recent_20_events)
end
context 'the wiki_events feature flag is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'omits the wiki page events when using to_a' do
events = described_class.new(projects).to_a
expect(events).not_to include(wiki_page_event)
end
it 'omits the wiki page events when using all_project_events' do
events = described_class.new(projects).all_project_events
expect(events).not_to include(wiki_page_event)
end
end
context 'the wiki_events feature flag is enabled' do
before do
stub_feature_flags(wiki_events: true)
end
it 'includes the wiki page events when using to_a' do
events = described_class.new(projects).to_a
expect(events).to include(wiki_page_event)
end
it 'includes the wiki page events when using all_project_events' do
events = described_class.new(projects).all_project_events
expect(events).to include(wiki_page_event)
end
end
it 'applies a limit to the number of events' do
@ -44,12 +90,25 @@ describe EventCollection do
expect(events).to be_empty
end
it 'allows filtering of events using an EventFilter' do
it 'allows filtering of events using an EventFilter, returning single item' do
filter = EventFilter.new(EventFilter::ISSUE)
events = described_class.new(projects, filter: filter).to_a
expect(events.length).to eq(1)
expect(events[0].action).to eq(Event::CLOSED)
expect(events).to contain_exactly(closed_issue_event)
end
it 'allows filtering of events using an EventFilter, returning several items' do
filter = EventFilter.new(EventFilter::COMMENTS)
events = described_class.new(projects, filter: filter).to_a
expect(events).to match_array(merge_request_events)
end
it 'allows filtering of events using an EventFilter, returning pushes' do
filter = EventFilter.new(EventFilter::PUSH)
events = described_class.new(projects, filter: filter).to_a
expect(events).to match_array(push_events)
end
end

View file

@ -454,9 +454,10 @@ describe Event do
end
end
describe '.for_wiki_page' do
describe 'wiki_page predicate scopes' do
let_it_be(:events) do
[
create(:push_event),
create(:closed_issue_event),
create(:wiki_page_event),
create(:closed_issue_event),
@ -465,10 +466,22 @@ describe Event do
]
end
it 'only contains the wiki page events' do
wiki_events = events.select(&:wiki_page?)
describe '.for_wiki_page' do
it 'only contains the wiki page events' do
wiki_events = events.select(&:wiki_page?)
expect(described_class.for_wiki_page).to match_array(wiki_events)
expect(events).not_to match_array(wiki_events)
expect(described_class.for_wiki_page).to match_array(wiki_events)
end
end
describe '.not_wiki_page' do
it 'does not contain the wiki page events' do
non_wiki_events = events.reject(&:wiki_page?)
expect(events).not_to match_array(non_wiki_events)
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
end
end
end

View file

@ -15,7 +15,7 @@ RSpec.describe Release do
it { is_expected.to have_many(:links).class_name('Releases::Link') }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:milestone_releases) }
it { is_expected.to have_one(:evidence) }
it { is_expected.to have_many(:evidences).class_name('Releases::Evidence') }
end
describe 'validation' do
@ -97,7 +97,7 @@ RSpec.describe Release do
describe '#create_evidence!' do
context 'when a release is created' do
it 'creates one Evidence object too' do
expect { release_with_evidence }.to change(Evidence, :count).by(1)
expect { release_with_evidence }.to change(Releases::Evidence, :count).by(1)
end
end
end
@ -106,7 +106,7 @@ RSpec.describe Release do
it 'also deletes the associated evidence' do
release_with_evidence
expect { release_with_evidence.destroy }.to change(Evidence, :count).by(-1)
expect { release_with_evidence.destroy }.to change(Releases::Evidence, :count).by(-1)
end
end
end
@ -155,7 +155,7 @@ RSpec.describe Release do
context 'when a release was created with evidence collection' do
let!(:release) { create(:release, :with_evidence) }
it { is_expected.to eq(release.evidence.summary_sha) }
it { is_expected.to eq(release.evidences.first.summary_sha) }
end
end
@ -171,7 +171,7 @@ RSpec.describe Release do
context 'when a release was created with evidence collection' do
let!(:release) { create(:release, :with_evidence) }
it { is_expected.to eq(release.evidence.summary) }
it { is_expected.to eq(release.evidences.first.summary) }
end
end

View file

@ -2,7 +2,7 @@
require 'spec_helper'
describe Evidence do
describe Releases::Evidence do
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
let(:schema_file) { 'evidences/evidence' }

View file

@ -112,28 +112,4 @@ describe ReleasePresenter do
it { is_expected.to be_nil }
end
end
describe '#evidence_file_path' do
subject { presenter.evidence_file_path }
context 'without evidence' do
it { is_expected.to be_falsy }
end
context 'with evidence' do
let(:release) { create :release, :with_evidence, project: project }
specify do
is_expected.to match /#{evidence_project_release_url(project, release.tag, format: :json)}/
end
end
context 'when a tag contains a slash' do
let(:release) { create :release, :with_evidence, project: project, tag: 'debian/2.4.0-1' }
specify do
is_expected.to match /#{evidence_project_release_url(project, CGI.escape(release.tag), format: :json)}/
end
end
end
end

View file

@ -114,6 +114,26 @@ describe API::Events do
expect(json_response.size).to eq(1)
end
context 'when the list of events includes wiki page events' do
it 'returns information about the wiki event', :aggregate_failures do
page = create(:wiki_page, project: private_project)
[Event::CREATED, Event::UPDATED, Event::DESTROYED].each do |action|
create(:wiki_page_event, wiki_page: page, action: action, author: user)
end
get api("/users/#{user.id}/events", user)
wiki_events = json_response.select { |e| e['target_type'] == 'WikiPage::Meta' }
action_names = wiki_events.map { |e| e['action_name'] }
titles = wiki_events.map { |e| e['target_title'] }
slugs = wiki_events.map { |e| e.dig('wiki_page', 'slug') }
expect(action_names).to contain_exactly('created', 'updated', 'destroyed')
expect(titles).to all(eq(page.title))
expect(slugs).to all(eq(page.slug))
end
end
context 'when the list of events includes push events' do
let(:event) do
create(:push_event, author: user, project: private_project)

View file

@ -51,6 +51,7 @@ describe 'getting group information', :do_not_mock_admin_mode do
it "returns one of user1's groups" do
project = create(:project, namespace: group2, path: 'Foo')
issue = create(:issue, project: create(:project, group: group1))
create(:project_group_link, project: project, group: group1)
post_graphql(group_query(group1), current_user: user1)
@ -67,6 +68,8 @@ describe 'getting group information', :do_not_mock_admin_mode do
expect(graphql_data['group']['fullName']).to eq(group1.full_name)
expect(graphql_data['group']['fullPath']).to eq(group1.full_path)
expect(graphql_data['group']['parentId']).to eq(group1.parent_id)
expect(graphql_data['group']['issues']['nodes'].count).to eq(1)
expect(graphql_data['group']['issues']['nodes'][0]['iid']).to eq(issue.iid.to_s)
end
it "does not return a non existing group" do

View file

@ -71,6 +71,7 @@ describe API::Groups do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['created_at']).to be_present
expect(json_response)
.to satisfy_one { |group| group['name'] == group1.name }
end
@ -121,6 +122,15 @@ describe API::Groups do
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
it "includes a created_at timestamp" do
get api("/groups", user1)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['created_at']).to be_present
end
end
context "when authenticated as admin" do
@ -152,6 +162,15 @@ describe API::Groups do
expect(json_response.first).not_to include('statistics')
end
it "includes a created_at timestamp" do
get api("/groups", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['created_at']).to be_present
end
it "includes statistics if requested" do
attributes = {
storage_size: 1158,
@ -357,6 +376,7 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include('runners_token')
expect(json_response).to include('created_at')
end
it 'returns only public projects in the group' do
@ -407,6 +427,7 @@ describe API::Groups do
expect(json_response['full_name']).to eq(group1.full_name)
expect(json_response['full_path']).to eq(group1.full_path)
expect(json_response['parent_id']).to eq(group1.parent_id)
expect(json_response['created_at']).to be_present
expect(json_response['projects']).to be_an Array
expect(json_response['projects'].length).to eq(2)
expect(json_response['shared_projects']).to be_an Array
@ -613,6 +634,7 @@ describe API::Groups do
expect(json_response['subgroup_creation_level']).to eq("maintainer")
expect(json_response['request_access_enabled']).to eq(true)
expect(json_response['parent_id']).to eq(nil)
expect(json_response['created_at']).to be_present
expect(json_response['projects']).to be_an Array
expect(json_response['projects'].length).to eq(2)
expect(json_response['shared_projects']).to be_an Array

View file

@ -104,6 +104,21 @@ describe API::Releases do
expect(json_response.first['upcoming_release']).to eq(false)
end
it 'avoids N+1 queries' do
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
control_count = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/releases", maintainer)
end.count
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
expect do
get api("/projects/#{project.id}/releases", maintainer)
end.not_to exceed_query_limit(control_count)
end
context 'when tag does not exist in git repository' do
let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
@ -725,7 +740,7 @@ describe API::Releases do
end
it 'does not create an Evidence object', :sidekiq_inline do
expect { subject }.not_to change(Evidence, :count)
expect { subject }.not_to change(Releases::Evidence, :count)
end
it 'is a historical release' do
@ -755,7 +770,7 @@ describe API::Releases do
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Evidence, :count).by(1)
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
@ -785,7 +800,7 @@ describe API::Releases do
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Evidence, :count).by(1)
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do

View file

@ -153,6 +153,46 @@ describe EventCreateService do
end
end
describe '#wiki_event' do
let_it_be(:user) { create(:user) }
let_it_be(:wiki_page) { create(:wiki_page) }
let_it_be(:meta) { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
Event::WIKI_ACTIONS.each do |action|
context "The action is #{action}" do
let(:event) { service.wiki_event(meta, user, action) }
it 'creates the event' do
expect(event).to have_attributes(
wiki_page?: true,
valid?: true,
persisted?: true,
action: action,
wiki_page: wiki_page
)
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not create the event' do
expect { event }.not_to change(Event, :count)
end
end
end
end
(Event::ACTIONS.values - Event::WIKI_ACTIONS).each do |bad_action|
context "The action is #{bad_action}" do
it 'raises an error' do
expect { service.wiki_event(meta, user, bad_action) }.to raise_error(described_class::IllegalActionError)
end
end
end
end
describe '#push', :clean_gitlab_redis_shared_state do
let(:project) { create(:project) }
let(:user) { create(:user) }

View file

@ -6,22 +6,24 @@ describe WikiPages::BaseService do
let(:project) { double('project') }
let(:user) { double('user') }
subject(:service) { described_class.new(project, user, {}) }
describe '#increment_usage' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
error = counter::UnknownEvent
it 'raises an error on unknown events' do
expect { subject.send(:increment_usage, :bad_event) }.to raise_error error
end
let(:subject) { bad_service_class.new(project, user, {}) }
context 'the event is valid' do
counter::KNOWN_EVENTS.each do |e|
it "updates the #{e} counter" do
expect { subject.send(:increment_usage, e) }.to change { counter.read(e) }
context 'the class implements usage_counter_action incorrectly' do
let(:bad_service_class) do
Class.new(described_class) do
def usage_counter_action
:bad_event
end
end
end
it 'raises an error on unknown events' do
expect { subject.send(:increment_usage) }.to raise_error(error)
end
end
end
end

View file

@ -5,19 +5,16 @@ require 'spec_helper'
describe WikiPages::CreateService do
let(:project) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:page_title) { 'Title' }
let(:opts) do
{
title: 'Title',
title: page_title,
content: 'Content for wiki page',
format: 'markdown'
}
end
let(:bad_opts) do
{ title: '' }
end
subject(:service) { described_class.new(project, user, opts) }
before do
@ -35,8 +32,7 @@ describe WikiPages::CreateService do
end
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once
.with(instance_of(WikiPage), 'create')
expect(service).to receive(:execute_hooks).once.with(WikiPage)
service.execute
end
@ -47,8 +43,41 @@ describe WikiPages::CreateService do
expect { service.execute }.to change { counter.read(:create) }.by 1
end
shared_examples 'correct event created' do
it 'creates appropriate events' do
expect { service.execute }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::CREATED,
target: have_attributes(canonical_slug: page_title)
)
end
end
context 'the new page is at the top level' do
let(:page_title) { 'root-level-page' }
include_examples 'correct event created'
end
context 'the new page is in a subsection' do
let(:page_title) { 'subsection/page' }
include_examples 'correct event created'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
subject(:service) { described_class.new(project, user, bad_opts) }
let(:page_title) { '' }
it 'does not count a creation event' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
@ -56,6 +85,10 @@ describe WikiPages::CreateService do
expect { service.execute }.not_to change { counter.read(:create) }
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
it 'reports the error' do
expect(service.execute).to be_invalid
.and have_attributes(errors: be_present)

View file

@ -15,8 +15,7 @@ describe WikiPages::DestroyService do
describe '#execute' do
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once
.with(instance_of(WikiPage), 'delete')
expect(service).to receive(:execute_hooks).once.with(page)
service.execute(page)
end
@ -27,10 +26,29 @@ describe WikiPages::DestroyService do
expect { service.execute(page) }.to change { counter.read(:delete) }.by 1
end
it 'creates a new wiki page deletion event' do
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::DESTROYED,
target: have_attributes(canonical_slug: page.slug)
)
end
it 'does not increment the delete count if the deletion failed' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
end
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
end

View file

@ -6,20 +6,17 @@ describe WikiPages::UpdateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
let(:page_title) { 'New Title' }
let(:opts) do
{
content: 'New content for wiki page',
format: 'markdown',
message: 'New wiki message',
title: 'New Title'
title: page_title
}
end
let(:bad_opts) do
{ title: '' }
end
subject(:service) { described_class.new(project, user, opts) }
before do
@ -34,12 +31,11 @@ describe WikiPages::UpdateService do
expect(updated_page.message).to eq(opts[:message])
expect(updated_page.content).to eq(opts[:content])
expect(updated_page.format).to eq(opts[:format].to_sym)
expect(updated_page.title).to eq(opts[:title])
expect(updated_page.title).to eq(page_title)
end
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once
.with(instance_of(WikiPage), 'update')
expect(service).to receive(:execute_hooks).once.with(WikiPage)
service.execute(page)
end
@ -50,8 +46,42 @@ describe WikiPages::UpdateService do
expect { service.execute page }.to change { counter.read(:update) }.by 1
end
shared_examples 'adds activity event' do
it 'adds a new wiki page activity event' do
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::UPDATED,
wiki_page: page,
target_title: page.title
)
end
end
context 'the page is at the top level' do
let(:page_title) { 'Top level page' }
include_examples 'adds activity event'
end
context 'the page is in a subsection' do
let(:page_title) { 'Subsection / secondary page' }
include_examples 'adds activity event'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
subject(:service) { described_class.new(project, user, bad_opts) }
let(:page_title) { '' }
it 'does not count an edit event' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
@ -59,6 +89,10 @@ describe WikiPages::UpdateService do
expect { service.execute page }.not_to change { counter.read(:update) }
end
it 'does not record the activity' do
expect { service.execute page }.not_to change(Event, :count)
end
it 'reports the error' do
expect(service.execute page).to be_invalid
.and have_attributes(errors: be_present)

View file

@ -8,6 +8,8 @@ module StubExperiments
# Examples
# - `stub_experiment(signup_flow: false)` ... Disable `signup_flow` experiment globally.
def stub_experiment(experiments)
allow(Gitlab::Experimentation).to receive(:enabled?).and_call_original
experiments.each do |experiment_key, enabled|
allow(Gitlab::Experimentation).to receive(:enabled?).with(experiment_key) { enabled }
end
@ -20,6 +22,8 @@ module StubExperiments
# Examples
# - `stub_experiment_for_user(signup_flow: false)` ... Disable `signup_flow` experiment for user.
def stub_experiment_for_user(experiments)
allow(Gitlab::Experimentation).to receive(:enabled_for_user?).and_call_original
experiments.each do |experiment_key, enabled|
allow(Gitlab::Experimentation).to receive(:enabled_for_user?).with(experiment_key, anything) { enabled }
end

View file

@ -5,7 +5,7 @@ require 'spec_helper'
describe CreateEvidenceWorker do
let!(:release) { create(:release) }
it 'creates a new Evidence' do
expect { described_class.new.perform(release.id) }.to change(Evidence, :count).by(1)
it 'creates a new Evidence record' do
expect { described_class.new.perform(release.id) }.to change(Releases::Evidence, :count).by(1)
end
end