summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 12:09:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 12:09:42 +0000
commit729e3765d5feb762df1ccfbc228a8dd4662aa3f9 (patch)
treef326420fc64999c6bcc28816ed54f0972fb46459
parent6f7881ee9dcec34141a8f34fc814b56b366d2b48 (diff)
downloadgitlab-ce-729e3765d5feb762df1ccfbc228a8dd4662aa3f9.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue37
-rw-r--r--app/assets/javascripts/notes/stores/actions.js5
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js9
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue102
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss17
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss20
-rw-r--r--app/controllers/projects/releases/evidences_controller.rb38
-rw-r--r--app/controllers/projects/releases_controller.rb14
-rw-r--r--app/finders/events_finder.rb9
-rw-r--r--app/graphql/resolvers/issues_resolver.rb11
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/helpers/nav_helper.rb4
-rw-r--r--app/models/event.rb9
-rw-r--r--app/models/event_collection.rb9
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/release.rb20
-rw-r--r--app/models/releases/evidence.rb (renamed from app/models/evidence.rb)6
-rw-r--r--app/policies/release_policy.rb27
-rw-r--r--app/policies/releases/evidence_policy.rb34
-rw-r--r--app/presenters/release_presenter.rb5
-rw-r--r--app/presenters/releases/evidence_presenter.rb16
-rw-r--r--app/services/event_create_service.rb15
-rw-r--r--app/services/wiki_pages/base_service.rb52
-rw-r--r--app/services/wiki_pages/create_service.rb14
-rw-r--r--app/services/wiki_pages/destroy_service.rb14
-rw-r--r--app/services/wiki_pages/update_service.rb21
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/workers/create_evidence_worker.rb2
-rw-r--r--changelogs/unreleased/197227-graphql-group-milestones.yml5
-rw-r--r--changelogs/unreleased/199065-support-on-demand-release-evidence.yml5
-rw-r--r--changelogs/unreleased/209854-cache-es-check.yml5
-rw-r--r--changelogs/unreleased/expose-created-at-in-groups-api.yml5
-rw-r--r--changelogs/unreleased/feat-add-toggle-all-discussions-button.yml5
-rw-r--r--changelogs/unreleased/sast-no-env-file.yml5
-rw-r--r--config/routes/project.rb4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql100
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json219
-rw-r--r--doc/api/groups.md9
-rw-r--r--doc/integration/elasticsearch.md9
-rw-r--r--lib/api/entities/event.rb1
-rw-r--r--lib/api/entities/group.rb1
-rw-r--r--lib/api/entities/release.rb2
-rw-r--r--lib/api/entities/releases/evidence.rb15
-rw-r--r--lib/api/helpers/presentable.rb3
-rw-r--r--lib/event_filter.rb19
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml4
-rw-r--r--lib/gitlab/experimentation.rb6
-rw-r--r--locale/gitlab.pot8
-rwxr-xr-xscripts/trigger-build-docs12
-rw-r--r--spec/controllers/projects/releases/evidences_controller_spec.rb165
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb145
-rw-r--r--spec/factories/events.rb4
-rw-r--r--spec/factories/evidences.rb2
-rw-r--r--spec/finders/events_finder_spec.rb30
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/evidence.json14
-rw-r--r--spec/frontend/locale/index_spec.js (renamed from spec/javascripts/locale/index_spec.js)12
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js59
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js46
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js15
-rw-r--r--spec/frontend/releases/mock_data.js23
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb35
-rw-r--r--spec/helpers/nav_helper_spec.rb20
-rw-r--r--spec/lib/api/entities/release_spec.rb34
-rw-r--r--spec/lib/event_filter_spec.rb50
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/models/event_collection_spec.rb81
-rw-r--r--spec/models/event_spec.rb21
-rw-r--r--spec/models/release_spec.rb10
-rw-r--r--spec/models/releases/evidence_spec.rb (renamed from spec/models/evidence_spec.rb)2
-rw-r--r--spec/presenters/release_presenter_spec.rb24
-rw-r--r--spec/requests/api/events_spec.rb20
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb3
-rw-r--r--spec/requests/api/groups_spec.rb22
-rw-r--r--spec/requests/api/releases_spec.rb21
-rw-r--r--spec/services/event_create_service_spec.rb40
-rw-r--r--spec/services/wiki_pages/base_service_spec.rb20
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb49
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb22
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb52
-rw-r--r--spec/support/helpers/stub_experiments.rb4
-rw-r--r--spec/workers/create_evidence_worker_spec.rb4
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