diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-24 12:09:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-24 12:09:42 +0000 |
commit | 729e3765d5feb762df1ccfbc228a8dd4662aa3f9 (patch) | |
tree | f326420fc64999c6bcc28816ed54f0972fb46459 | |
parent | 6f7881ee9dcec34141a8f34fc814b56b366d2b48 (diff) | |
download | gitlab-ce-729e3765d5feb762df1ccfbc228a8dd4662aa3f9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
88 files changed, 1616 insertions, 418 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index b379f0ee722..fd0b626919a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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 diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 577612de06a..c28ac94b3ed 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -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> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 2e6719bb4fb..accc37121d0 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -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'); }); }; diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 6554aee0d5b..0cc59f9150c 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index c23ef93c056..68bf8394508 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -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); diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue index 0c51fffc96c..59c1b3eb48e 100644 --- a/app/assets/javascripts/releases/components/evidence_block.vue +++ b/app/assets/javascripts/releases/components/evidence_block.vue @@ -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 }); + }, + evidenceUrl(index) { + return this.release.evidences[index].filepath; + }, + sha(index) { + return this.release.evidences[index].sha; }, - shortSha() { - return truncateSha(this.sha); + shortSha(index) { + return truncateSha(this.release.evidences[index].sha); }, - sha() { - return this.release.evidenceSha; + 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(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> - <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" - /> + <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> diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 61cd22dc161..515aa629476 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -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 || []; diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 418eafa153c..d0c3de59937 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -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, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5ae4f72de56..dd338a7134b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -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; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index aaecbd6ff00..f2b8433a995 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -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; diff --git a/app/controllers/projects/releases/evidences_controller.rb b/app/controllers/projects/releases/evidences_controller.rb new file mode 100644 index 00000000000..34e450d903f --- /dev/null +++ b/app/controllers/projects/releases/evidences_controller.rb @@ -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 diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 7d6b38dd243..fc60f42095c 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -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 diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index 7755cbdf9e5..9c56451fd44 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -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)) diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index ae77af32b5b..04da54a6bb6 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -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? diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index bd9efef94f8..20b4c66ba95 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -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 diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 6013475acb1..7d48efcff01 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -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 diff --git a/app/models/event.rb b/app/models/event.rb index c4ca5389fdf..447ab753421 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -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 diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb index 4768506b8fa..4c178e27b75 100644 --- a/app/models/event_collection.rb +++ b/app/models/event_collection.rb @@ -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 diff --git a/app/models/issue.rb b/app/models/issue.rb index 3d389013985..bdcebb4b942 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bb7afc49cd8..7934b0f8f59 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -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 diff --git a/app/models/release.rb b/app/models/release.rb index 45c2a56d764..403087a2cad 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -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 evidence_sha - evidence&.summary_sha + def milestone_titles + self.milestones.map {|m| m.title }.sort.join(", ") end - def evidence_summary - evidence&.summary || {} + def evidence_sha + evidences.first&.summary_sha end - def milestone_titles - self.milestones.map {|m| m.title }.sort.join(", ") + def evidence_summary + evidences.first&.summary || {} end private diff --git a/app/models/evidence.rb b/app/models/releases/evidence.rb index 55149ab0dfa..1aac7e33e41 100644 --- a/app/models/evidence.rb +++ b/app/models/releases/evidence.rb @@ -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) diff --git a/app/policies/release_policy.rb b/app/policies/release_policy.rb index 0fd1312c511..d7f9e5d7445 100644 --- a/app/policies/release_policy.rb +++ b/app/policies/release_policy.rb @@ -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 diff --git a/app/policies/releases/evidence_policy.rb b/app/policies/releases/evidence_policy.rb new file mode 100644 index 00000000000..701913e6fe4 --- /dev/null +++ b/app/policies/releases/evidence_policy.rb @@ -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 diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index 8cf7446ce64..3db89df1cc8 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -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 diff --git a/app/presenters/releases/evidence_presenter.rb b/app/presenters/releases/evidence_presenter.rb new file mode 100644 index 00000000000..a00cbacb7d8 --- /dev/null +++ b/app/presenters/releases/evidence_presenter.rb @@ -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 diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 7460f0df535..0b044e1679a 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -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) diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb index 82c15ffc9b9..2e774973ca5 100644 --- a/app/services/wiki_pages/base_service.rb +++ b/app/services/wiki_pages/base_service.rb @@ -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 diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb index 2e2e0fd9033..811f460e042 100644 --- a/app/services/wiki_pages/create_service.rb +++ b/app/services/wiki_pages/create_service.rb @@ -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 diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb index 3f9343339cd..eb162223723 100644 --- a/app/services/wiki_pages/destroy_service.rb +++ b/app/services/wiki_pages/destroy_service.rb @@ -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 diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb index 2159dd91e9c..0a056f1ec33 100644 --- a/app/services/wiki_pages/update_service.rb +++ b/app/services/wiki_pages/update_service.rb @@ -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 diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 5719fb24b89..202a4018050 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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' diff --git a/app/workers/create_evidence_worker.rb b/app/workers/create_evidence_worker.rb index c2faba84cfc..135e2ac38b4 100644 --- a/app/workers/create_evidence_worker.rb +++ b/app/workers/create_evidence_worker.rb @@ -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 diff --git a/changelogs/unreleased/197227-graphql-group-milestones.yml b/changelogs/unreleased/197227-graphql-group-milestones.yml new file mode 100644 index 00000000000..8acd40ca2f0 --- /dev/null +++ b/changelogs/unreleased/197227-graphql-group-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Add issues to graphQL group endpoint +merge_request: 27789 +author: +type: added diff --git a/changelogs/unreleased/199065-support-on-demand-release-evidence.yml b/changelogs/unreleased/199065-support-on-demand-release-evidence.yml new file mode 100644 index 00000000000..99aebcb8b98 --- /dev/null +++ b/changelogs/unreleased/199065-support-on-demand-release-evidence.yml @@ -0,0 +1,5 @@ +--- +title: Support multiple Evidences for a Release +merge_request: 26509 +author: +type: changed diff --git a/changelogs/unreleased/209854-cache-es-check.yml b/changelogs/unreleased/209854-cache-es-check.yml new file mode 100644 index 00000000000..e8a171fa990 --- /dev/null +++ b/changelogs/unreleased/209854-cache-es-check.yml @@ -0,0 +1,5 @@ +--- +title: Cache ES enabled namespaces and projects +merge_request: 27348 +author: +type: performance diff --git a/changelogs/unreleased/expose-created-at-in-groups-api.yml b/changelogs/unreleased/expose-created-at-in-groups-api.yml new file mode 100644 index 00000000000..2e3520d7afa --- /dev/null +++ b/changelogs/unreleased/expose-created-at-in-groups-api.yml @@ -0,0 +1,5 @@ +--- +title: Expose created_at property in Groups API +merge_request: 27824 +author: +type: added diff --git a/changelogs/unreleased/feat-add-toggle-all-discussions-button.yml b/changelogs/unreleased/feat-add-toggle-all-discussions-button.yml new file mode 100644 index 00000000000..803e517d2de --- /dev/null +++ b/changelogs/unreleased/feat-add-toggle-all-discussions-button.yml @@ -0,0 +1,5 @@ +--- +title: Add toggle all discussions button to MRs +merge_request: 24670 +author: Martin Hobert & Diego Louzán +type: added diff --git a/changelogs/unreleased/sast-no-env-file.yml b/changelogs/unreleased/sast-no-env-file.yml new file mode 100644 index 00000000000..86a47effd68 --- /dev/null +++ b/changelogs/unreleased/sast-no-env-file.yml @@ -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 diff --git a/config/routes/project.rb b/config/routes/project.rb index b86fd48e222..4b2bac97678 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -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 diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index ea11a203921..0fbd8c84e58 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -3220,6 +3220,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 """ lfsEnabled: Boolean diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 9e3460c0b03..bd78b51684f 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -9243,6 +9243,225 @@ "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", "args": [ diff --git a/doc/api/groups.md b/doc/api/groups.md index 235f7f4081a..fbad7f3f11b 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -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, diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index d4ec30e4938..60d6274cfce 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -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 diff --git a/lib/api/entities/event.rb b/lib/api/entities/event.rb index 9c2d766b7f1..8fd0bac13f4 100644 --- a/lib/api/entities/event.rb +++ b/lib/api/entities/event.rb @@ -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, diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb index 10e10e52d9f..8a6a5b7057c 100644 --- a/lib/api/entities/group.rb +++ b/lib/api/entities/group.rb @@ -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 diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb index c70982a9ece..edcd9bc6505 100644 --- a/lib/api/entities/release.rb +++ b/lib/api/entities/release.rb @@ -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 diff --git a/lib/api/entities/releases/evidence.rb b/lib/api/entities/releases/evidence.rb new file mode 100644 index 00000000000..25b2bf6bf6f --- /dev/null +++ b/lib/api/entities/releases/evidence.rb @@ -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 diff --git a/lib/api/helpers/presentable.rb b/lib/api/helpers/presentable.rb index 973c2132efe..a5186cc56ea 100644 --- a/lib/api/helpers/presentable.rb +++ b/lib/api/helpers/presentable.rb @@ -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 diff --git a/lib/event_filter.rb b/lib/event_filter.rb index e062e3ddb1c..8cb0b1441df 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -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 diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 9f9975f9e1c..262c52b2484 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -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 diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 30c8eaf605a..f1b952760b5 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 28b568ae8a2..cb8ba1baab7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs index 70e7150d475..2957dde6fc0 100755 --- a/scripts/trigger-build-docs +++ b/scripts/trigger-build-docs @@ -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 diff --git a/spec/controllers/projects/releases/evidences_controller_spec.rb b/spec/controllers/projects/releases/evidences_controller_spec.rb new file mode 100644 index 00000000000..d3808087681 --- /dev/null +++ b/spec/controllers/projects/releases/evidences_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index ca073c520cd..4c957e22d24 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -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 diff --git a/spec/factories/events.rb b/spec/factories/events.rb index b4285627de3..5b456bb58ff 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -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 diff --git a/spec/factories/evidences.rb b/spec/factories/evidences.rb index 964f232a1c9..77116d8e9ed 100644 --- a/spec/factories/evidences.rb +++ b/spec/factories/evidences.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true FactoryBot.define do - factory :evidence do + factory :evidence, class: 'Releases::Evidence' do release end end diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb index 5c28b31e8c8..443e9ab4bc4 100644 --- a/spec/finders/events_finder_spec.rb +++ b/spec/finders/events_finder_spec.rb @@ -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) diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json index a239be09919..02e23d2732d 100644 --- a/spec/fixtures/api/schemas/public_api/v4/release.json +++ b/spec/fixtures/api/schemas/public_api/v4/release.json @@ -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": { diff --git a/spec/fixtures/api/schemas/public_api/v4/release/evidence.json b/spec/fixtures/api/schemas/public_api/v4/release/evidence.json new file mode 100644 index 00000000000..fbebac0acaa --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/release/evidence.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "required" : [ + "sha", + "filepath", + "collected_at" + ], + "properties" : { + "sha": { "type": "string" }, + "filepath": { "type": "string" }, + "collected_at": { "type": "date" } + }, + "additionalProperties": false +} diff --git a/spec/javascripts/locale/index_spec.js b/spec/frontend/locale/index_spec.js index 29b0b21eed7..346ed5182f4 100644 --- a/spec/javascripts/locale/index_spec.js +++ b/spec/frontend/locale/index_spec.js @@ -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'); }); }); }); diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index c9375df07e8..77603c16f82 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -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); + }); + }); + }); }); diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index ee772afbc03..ea5658821b1 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -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 = { diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js index c76a0e04dce..ba60a79e464 100644 --- a/spec/frontend/releases/components/evidence_block_spec.js +++ b/spec/frontend/releases/components/evidence_block_spec.js @@ -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, ); }); }); diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js index 85e6bab71ba..bd5fc86275e 100644 --- a/spec/frontend/releases/mock_data.js +++ b/spec/frontend/releases/mock_data.js @@ -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', diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 7cfef9b4cc7..4467c228e96 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -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, _| diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb index f92dca11136..6bdbec1203c 100644 --- a/spec/helpers/nav_helper_spec.rb +++ b/spec/helpers/nav_helper_spec.rb @@ -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 diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb index f0bbaa35efe..c45dbc15856 100644 --- a/spec/lib/api/entities/release_spec.rb +++ b/spec/lib/api/entities/release_spec.rb @@ -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) diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index e35698f6030..da6e1f9458f 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -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 diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index b3f279344b1..0ed5a1e7b49 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -94,7 +94,7 @@ releases: - links - milestone_releases - milestones -- evidence +- evidences links: - release project_members: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 5b0444c394e..533458afd73 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -134,7 +134,7 @@ Release: - created_at - updated_at - released_at -Evidence: +Releases::Evidence: - id - summary - created_at diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb index e6f80a4c4d0..6d1954700bf 100644 --- a/spec/models/event_collection_spec.rb +++ b/spec/models/event_collection_spec.rb @@ -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 diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index b2676a79b55..3239c7a843a 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -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 diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 3884b8138be..8b1b738ab58 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -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 diff --git a/spec/models/evidence_spec.rb b/spec/models/releases/evidence_spec.rb index 8f534517fc1..d38d2021117 100644 --- a/spec/models/evidence_spec.rb +++ b/spec/models/releases/evidence_spec.rb @@ -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' } diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb index 82f312622ff..d1f023b8760 100644 --- a/spec/presenters/release_presenter_spec.rb +++ b/spec/presenters/release_presenter_spec.rb @@ -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 diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index acf3bb3482a..decdcc66327 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -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) diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index a38d1857076..c7b537a9923 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -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 diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index cec4995c620..ea60f783b48 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -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 diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 41999ca6e60..e66e999dc27 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -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 diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index a8ddca0cdf3..0a8a4d5bf58 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -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) } diff --git a/spec/services/wiki_pages/base_service_spec.rb b/spec/services/wiki_pages/base_service_spec.rb index 2e70246c6f2..4c44c195ac8 100644 --- a/spec/services/wiki_pages/base_service_spec.rb +++ b/spec/services/wiki_pages/base_service_spec.rb @@ -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 diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb index ef03a2e9788..d63d62e9492 100644 --- a/spec/services/wiki_pages/create_service_spec.rb +++ b/spec/services/wiki_pages/create_service_spec.rb @@ -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) diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb index 350a7eb123b..e205bedfdb9 100644 --- a/spec/services/wiki_pages/destroy_service_spec.rb +++ b/spec/services/wiki_pages/destroy_service_spec.rb @@ -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 diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb index d5f46e7b2db..3eb486833e6 100644 --- a/spec/services/wiki_pages/update_service_spec.rb +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -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) diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb index 7a5a188ab4d..ff3b02dc3f6 100644 --- a/spec/support/helpers/stub_experiments.rb +++ b/spec/support/helpers/stub_experiments.rb @@ -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 diff --git a/spec/workers/create_evidence_worker_spec.rb b/spec/workers/create_evidence_worker_spec.rb index 364b2098251..9b8314122cd 100644 --- a/spec/workers/create_evidence_worker_spec.rb +++ b/spec/workers/create_evidence_worker_spec.rb @@ -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 |