Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6f7881ee9d
commit
729e3765d5
88 changed files with 1616 additions and 418 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 || [];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
38
app/controllers/projects/releases/evidences_controller.rb
Normal file
38
app/controllers/projects/releases/evidences_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
34
app/policies/releases/evidence_policy.rb
Normal file
34
app/policies/releases/evidence_policy.rb
Normal 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
|
|
@ -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
|
||||
|
|
16
app/presenters/releases/evidence_presenter.rb
Normal file
16
app/presenters/releases/evidence_presenter.rb
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add issues to graphQL group endpoint
|
||||
merge_request: 27789
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support multiple Evidences for a Release
|
||||
merge_request: 26509
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/209854-cache-es-check.yml
Normal file
5
changelogs/unreleased/209854-cache-es-check.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cache ES enabled namespaces and projects
|
||||
merge_request: 27348
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose created_at property in Groups API
|
||||
merge_request: 27824
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add toggle all discussions button to MRs
|
||||
merge_request: 24670
|
||||
author: Martin Hobert & Diego Louzán
|
||||
type: added
|
5
changelogs/unreleased/sast-no-env-file.yml
Normal file
5
changelogs/unreleased/sast-no-env-file.yml
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
15
lib/api/entities/releases/evidence.rb
Normal file
15
lib/api/entities/releases/evidence.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
165
spec/controllers/projects/releases/evidences_controller_spec.rb
Normal file
165
spec/controllers/projects/releases/evidences_controller_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :evidence do
|
||||
factory :evidence, class: 'Releases::Evidence' do
|
||||
release
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": {
|
||||
|
|
14
spec/fixtures/api/schemas/public_api/v4/release/evidence.json
vendored
Normal file
14
spec/fixtures/api/schemas/public_api/v4/release/evidence.json
vendored
Normal 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
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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, _|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ releases:
|
|||
- links
|
||||
- milestone_releases
|
||||
- milestones
|
||||
- evidence
|
||||
- evidences
|
||||
links:
|
||||
- release
|
||||
project_members:
|
||||
|
|
|
@ -134,7 +134,7 @@ Release:
|
|||
- created_at
|
||||
- updated_at
|
||||
- released_at
|
||||
Evidence:
|
||||
Releases::Evidence:
|
||||
- id
|
||||
- summary
|
||||
- created_at
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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' }
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue