diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-06 14:21:56 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-06 14:21:56 +0000 |
commit | 65eb469990d7eb6cb974e438e8c782da9c20db8d (patch) | |
tree | dedb404455f5b1f8b0b69cd592c6eac51572d3e1 | |
parent | d949826fef468bd4cae3c473e7924d2f2e5b11ec (diff) | |
download | gitlab-ce-65eb469990d7eb6cb974e438e8c782da9c20db8d.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
61 files changed, 857 insertions, 722 deletions
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 4e44cb5e6f4..a6185746bb1 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -665,5 +665,25 @@ export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) { return dispatch('fetchDiffFilesMeta'); } +export function moveToNeighboringCommit({ dispatch, state }, { direction }) { + const previousCommitId = state.commit?.prev_commit_id; + const nextCommitId = state.commit?.next_commit_id; + const canMove = { + next: !state.isLoading && nextCommitId, + previous: !state.isLoading && previousCommitId, + }; + let commitId; + + if (direction === 'next' && canMove.next) { + commitId = nextCommitId; + } else if (direction === 'previous' && canMove.previous) { + commitId = previousCommitId; + } + + if (commitId) { + dispatch('changeCurrentCommit', { commitId }); + } +} + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index b8b3a4f44fd..6edcb974670 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -329,7 +329,7 @@ export default { }, deleteIssuable(payload) { - this.service + return this.service .deleteIssuable(payload) .then(res => res.data) .then(data => { @@ -340,7 +340,7 @@ export default { }) .catch(() => { createFlash( - sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }), + sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }), ); }); }, @@ -365,7 +365,12 @@ export default { :issuable-type="issuableType" /> - <recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptchaModal" /> + <recaptcha-modal + v-show="showRecaptcha" + ref="recaptchaModal" + :html="recaptchaHTML" + @close="closeRecaptchaModal" + /> </div> <div v-else> <title-component diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index 2580f8e86b1..ec9c800b7a2 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -15,19 +15,6 @@ export default () => { notesApp, }, store, - data() { - const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; - const noteableData = JSON.parse(notesDataset.noteableData); - noteableData.noteableType = notesDataset.noteableType; - noteableData.targetType = notesDataset.targetType; - - return { - noteableData, - currentUserData: JSON.parse(notesDataset.currentUserData), - notesData: JSON.parse(notesDataset.notesData), - helpPagePath: notesDataset.helpPagePath, - }; - }, computed: { ...mapGetters(['discussionTabCounter']), ...mapState({ @@ -67,6 +54,19 @@ export default () => { updateDiscussionTabCounter() { this.notesCountBadge.text(this.discussionTabCounter); }, + dataset() { + const data = this.$el.dataset; + const noteableData = JSON.parse(data.noteableData); + noteableData.noteableType = data.noteableType; + noteableData.targetType = data.targetType; + + return { + noteableData, + notesData: JSON.parse(data.notesData), + userData: JSON.parse(data.currentUserData), + helpPagePath: data.helpPagePath, + }; + }, }, render(createElement) { // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`, @@ -76,11 +76,8 @@ export default () => { return createElement(discussionKeyboardNavigator, [ createElement('notes-app', { props: { - noteableData: this.noteableData, - notesData: this.notesData, - userData: this.currentUserData, + ...this.dataset(), shouldShow: this.isShowTabActive, - helpPagePath: this.helpPagePath, }, }), ]); diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 8f9e2359e0d..6fd3cee5340 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -14,38 +14,36 @@ document.addEventListener('DOMContentLoaded', () => { notesApp, }, store, - data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; - const parsedUserData = JSON.parse(notesDataset.currentUserData); - const noteableData = JSON.parse(notesDataset.noteableData); - let currentUserData = {}; + methods: { + setData() { + const notesDataset = this.$el.dataset; + const parsedUserData = JSON.parse(notesDataset.currentUserData); + const noteableData = JSON.parse(notesDataset.noteableData); + let currentUserData = {}; - noteableData.noteableType = notesDataset.noteableType; - noteableData.targetType = notesDataset.targetType; + noteableData.noteableType = notesDataset.noteableType; + noteableData.targetType = notesDataset.targetType; - if (parsedUserData) { - currentUserData = { - id: parsedUserData.id, - name: parsedUserData.name, - username: parsedUserData.username, - avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, - path: parsedUserData.path, - }; - } + if (parsedUserData) { + currentUserData = { + id: parsedUserData.id, + name: parsedUserData.name, + username: parsedUserData.username, + avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, + path: parsedUserData.path, + }; + } - return { - noteableData, - currentUserData, - notesData: JSON.parse(notesDataset.notesData), - }; + return { + noteableData, + userData: currentUserData, + notesData: JSON.parse(notesDataset.notesData), + }; + }, }, render(createElement) { return createElement('notes-app', { - props: { - noteableData: this.noteableData, - notesData: this.notesData, - userData: this.currentUserData, - }, + props: { ...this.setData() }, }); }, }); diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue index 3de4a4a27cf..8a9fff47623 100644 --- a/app/assets/javascripts/static_site_editor/pages/home.vue +++ b/app/assets/javascripts/static_site_editor/pages/home.vue @@ -2,6 +2,8 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import { GlSkeletonLoader } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import EditArea from '../components/edit_area.vue'; import EditHeader from '../components/edit_header.vue'; import SavedChangesMessage from '../components/saved_changes_message.vue'; @@ -11,6 +13,7 @@ import SubmitChangesError from '../components/submit_changes_error.vue'; export default { components: { + RichContentEditor, EditArea, EditHeader, InvalidContentMessage, @@ -19,6 +22,7 @@ export default { PublishToolbar, SubmitChangesError, }, + mixins: [glFeatureFlagsMixin()], computed: { ...mapState([ 'content', @@ -76,7 +80,14 @@ export default { @dismiss="dismissSubmitChangesError" /> <edit-header class="w-75 align-self-center py-2" :title="title" /> + <rich-content-editor + v-if="glFeatures.richContentEditor" + class="w-75 gl-align-self-center" + :value="content" + @input="setContent" + /> <edit-area + v-else class="w-75 h-100 shadow-none align-self-center" :value="content" @input="setContent" diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js index 01c5329b4d7..3e8f3dd548f 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js @@ -23,3 +23,5 @@ export const EDITOR_OPTIONS = { export const EDITOR_TYPES = { wysiwyg: 'wysiwyg', }; + +export const EDITOR_HEIGHT = '100%'; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue index e02d8661ceb..0b10424ad1e 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue @@ -2,7 +2,7 @@ import 'codemirror/lib/codemirror.css'; import '@toast-ui/editor/dist/toastui-editor.css'; -import { EDITOR_OPTIONS, EDITOR_TYPES } from './constants'; +import { EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT } from './constants'; export default { components: { @@ -16,6 +16,26 @@ export default { type: String, required: true, }, + options: { + type: Object, + required: false, + default: () => EDITOR_OPTIONS, + }, + initialEditType: { + type: String, + required: false, + default: EDITOR_TYPES.wysiwyg, + }, + height: { + type: String, + required: false, + default: EDITOR_HEIGHT, + }, + }, + computed: { + editorOptions() { + return { ...EDITOR_OPTIONS, ...this.options }; + }, }, methods: { onContentChanged() { @@ -25,16 +45,15 @@ export default { return this.$refs.editor.invoke('getMarkdown'); }, }, - editorOptions: EDITOR_OPTIONS, - initialEditType: EDITOR_TYPES.wysiwyg, }; </script> <template> <toast-editor ref="editor" - :initial-edit-type="$options.initialEditType" :initial-value="value" - :options="$options.editorOptions" + :options="editorOptions" + :initial-edit-type="initialEditType" + :height="height" @change="onContentChanged" /> </template> diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb index 8bbcecaa0af..4d0ad9fa02f 100644 --- a/app/controllers/projects/alert_management_controller.rb +++ b/app/controllers/projects/alert_management_controller.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class Projects::AlertManagementController < Projects::ApplicationController - before_action :ensure_feature_enabled + before_action :ensure_list_feature_enabled, only: :index + before_action :ensure_detail_feature_enabled, only: :details def index end @@ -11,7 +12,11 @@ class Projects::AlertManagementController < Projects::ApplicationController private - def ensure_feature_enabled + def ensure_list_feature_enabled render_404 unless Feature.enabled?(:alert_management_minimal, project) end + + def ensure_detail_feature_enabled + render_404 unless Feature.enabled?(:alert_management_detail, project) + end end diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb index 74f28c3da67..c91eece9983 100644 --- a/app/controllers/projects/static_site_editor_controller.rb +++ b/app/controllers/projects/static_site_editor_controller.rb @@ -10,6 +10,10 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController before_action :assign_ref_and_path, only: [:show] before_action :authorize_edit_tree!, only: [:show] + before_action do + push_frontend_feature_flag(:rich_content_editor) + end + def show @config = Gitlab::StaticSiteEditor::Config.new(@repository, @ref, @path, params[:return_url]) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index abf75a21404..d51ffa70035 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -869,6 +869,14 @@ module Ci end end + def collect_accessibility_reports!(accessibility_report) + each_report(Ci::JobArtifact::ACCESSIBILITY_REPORT_FILE_TYPES) do |file_type, blob| + Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, accessibility_report) + end + + accessibility_report + end + def collect_coverage_reports!(coverage_report) each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob| Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, coverage_report) diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb new file mode 100644 index 00000000000..22ad08b8238 --- /dev/null +++ b/app/models/ci/daily_build_group_report_result.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Ci + class DailyBuildGroupReportResult < ApplicationRecord + extend Gitlab::Ci::Model + + belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id + belongs_to :project + + def self.upsert_reports(data) + upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any? + end + end +end diff --git a/app/models/ci/daily_report_result.rb b/app/models/ci/daily_report_result.rb deleted file mode 100644 index 3c1c5f11ed4..00000000000 --- a/app/models/ci/daily_report_result.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Ci - class DailyReportResult < ApplicationRecord - extend Gitlab::Ci::Model - - belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id - belongs_to :project - - # TODO: Refactor this out when BuildReportResult is implemented. - # They both need to share the same enum values for param. - REPORT_PARAMS = { - coverage: 0 - }.freeze - - enum param_type: REPORT_PARAMS - - def self.upsert_reports(data) - upsert_all(data, unique_by: :index_daily_report_results_unique_columns) if data.any? - end - end -end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 1f3d47997f7..e2ef9e3dd5f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -82,7 +82,7 @@ module Ci has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline - has_many :daily_report_results, class_name: 'Ci::DailyReportResult', foreign_key: :last_pipeline_id + has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult', foreign_key: :last_pipeline_id accepts_nested_attributes_for :variables, reject_if: :persisted? @@ -194,7 +194,7 @@ module Ci # We wait a little bit to ensure that all BuildFinishedWorkers finish first # because this is where some metrics like code coverage is parsed and stored # in CI build records which the daily build metrics worker relies on. - pipeline.run_after_commit { Ci::DailyReportResultsWorker.perform_in(10.minutes, pipeline.id) } + pipeline.run_after_commit { Ci::DailyBuildGroupReportResultsWorker.perform_in(10.minutes, pipeline.id) } end after_transition do |pipeline, transition| diff --git a/app/models/group.rb b/app/models/group.rb index fd0f9cc223c..ea3c4c969cc 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -478,16 +478,6 @@ class Group < Namespace false end - def wiki_access_level - # TODO: Remove this method once we implement group-level features. - # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 - if Feature.enabled?(:group_wiki, self) - ProjectFeature::ENABLED - else - ProjectFeature::DISABLED - end - end - private def update_two_factor_requirement diff --git a/app/models/project.rb b/app/models/project.rb index 815124360e2..d76c1ee343a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -322,7 +322,7 @@ class Project < ApplicationRecord has_many :import_failures, inverse_of: :project has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project - has_many :daily_report_results, class_name: 'Ci::DailyReportResult' + has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult' has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove' diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 7d503b49c14..136ac4cce63 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class GroupPolicy < BasePolicy - include CrudPolicyHelpers include FindGroupProjects desc "Group is public" @@ -43,23 +42,15 @@ class GroupPolicy < BasePolicy @subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS end - desc "Group has wiki disabled" - condition(:wiki_disabled, score: 32) { !feature_available?(:wiki) } - rule { public_group }.policy do enable :read_group enable :read_package - enable :read_wiki end - rule { logged_in_viewable }.policy do - enable :read_group - enable :read_wiki - end + rule { logged_in_viewable }.enable :read_group rule { guest }.policy do enable :read_group - enable :read_wiki enable :upload_file end @@ -87,13 +78,11 @@ class GroupPolicy < BasePolicy enable :create_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation - enable :create_wiki end rule { reporter }.policy do enable :reporter_access enable :read_container_image - enable :download_wiki_code enable :admin_label enable :admin_list enable :admin_issue @@ -112,7 +101,6 @@ class GroupPolicy < BasePolicy enable :destroy_deploy_token enable :read_deploy_token enable :create_deploy_token - enable :admin_wiki end rule { owner }.policy do @@ -159,11 +147,6 @@ class GroupPolicy < BasePolicy rule { maintainer & can?(:create_projects) }.enable :transfer_projects - rule { wiki_disabled }.policy do - prevent(*create_read_update_admin_destroy(:wiki)) - prevent(:download_wiki_code) - end - def access_level return GroupMember::NO_ACCESS if @user.nil? @@ -173,21 +156,6 @@ class GroupPolicy < BasePolicy def lookup_access_level! @subject.max_member_access_for_user(@user) end - - # TODO: Extract this into a helper shared with ProjectPolicy, once we implement group-level features. - # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 - def feature_available?(feature) - return false unless feature == :wiki - - case @subject.wiki_access_level - when ProjectFeature::DISABLED - false - when ProjectFeature::PRIVATE - admin? || access_level >= ProjectFeature.required_minimum_access_level(feature) - else - true - end - end end GroupPolicy.prepend_if_ee('EE::GroupPolicy') diff --git a/app/services/ci/daily_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb index b774a806203..bacf96627f7 100644 --- a/app/services/ci/daily_report_result_service.rb +++ b/app/services/ci/daily_build_group_report_result_service.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true module Ci - class DailyReportResultService + class DailyBuildGroupReportResultService def execute(pipeline) return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true) - DailyReportResult.upsert_reports(coverage_reports(pipeline)) + DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline)) end private @@ -14,15 +14,14 @@ module Ci base_attrs = { project_id: pipeline.project_id, ref_path: pipeline.source_ref_path, - param_type: DailyReportResult.param_types[:coverage], date: pipeline.created_at.to_date, last_pipeline_id: pipeline.id } aggregate(pipeline.builds.with_coverage).map do |group_name, group| base_attrs.merge( - title: group_name, - value: average_coverage(group) + group_name: group_name, + data: { coverage: average_coverage(group) } ) end end diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb index c0de1c7c961..6e82a39ffd8 100644 --- a/app/services/groups/import_export/export_service.rb +++ b/app/services/groups/import_export/export_service.rb @@ -65,7 +65,7 @@ module Groups end def tree_exporter_class - if ::Feature.enabled?(:group_import_export_ndjson, @group&.parent) + if ::Feature.enabled?(:group_export_ndjson, @group&.parent) Gitlab::ImportExport::Group::TreeSaver else Gitlab::ImportExport::Group::LegacyTreeSaver diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb index c122da389ac..5e00ce9ccc0 100644 --- a/app/services/groups/import_export/import_service.rb +++ b/app/services/groups/import_export/import_service.rb @@ -53,7 +53,7 @@ module Groups end def ndjson? - ::Feature.enabled?(:group_import_export_ndjson, @group&.parent) && + ::Feature.enabled?(:group_import_ndjson, @group&.parent) && File.exist?(File.join(@shared.export_path, 'tree/groups/_all.ndjson')) end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index fa205f4f3d0..ccac944605d 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -682,7 +682,7 @@ :resource_boundary: :unknown :weight: 1 :idempotent: -- :name: pipeline_background:ci_daily_report_results +- :name: pipeline_background:ci_daily_build_group_report_results :feature_category: :continuous_integration :has_external_dependencies: :urgency: :low diff --git a/app/workers/ci/daily_report_results_worker.rb b/app/workers/ci/daily_build_group_report_results_worker.rb index 314fd44f86c..a6d3c485e24 100644 --- a/app/workers/ci/daily_report_results_worker.rb +++ b/app/workers/ci/daily_build_group_report_results_worker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Ci - class DailyReportResultsWorker + class DailyBuildGroupReportResultsWorker include ApplicationWorker include PipelineBackgroundQueue @@ -9,7 +9,7 @@ module Ci def perform(pipeline_id) Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline| - Ci::DailyReportResultService.new.execute(pipeline) + Ci::DailyBuildGroupReportResultService.new.execute(pipeline) end end end diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb index 79a6ca5e18a..eb44ff00d09 100644 --- a/config/initializers/action_cable.rb +++ b/config/initializers/action_cable.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true Rails.application.configure do - # Prevents the default engine from being mounted because - # we're running ActionCable as a standalone server - config.action_cable.mount_path = nil + # We only mount the ActionCable engine in tests where we run it in-app + # For other environments, we run it on a standalone Puma server + config.action_cable.mount_path = Rails.env.test? ? '/-/cable' : nil config.action_cable.url = Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/cable') config.action_cable.worker_pool_size = Gitlab.config.action_cable.worker_pool_size end diff --git a/db/migrate/20200421111005_create_daily_build_group_report_results.rb b/db/migrate/20200421111005_create_daily_build_group_report_results.rb new file mode 100644 index 00000000000..12d1c7531d5 --- /dev/null +++ b/db/migrate/20200421111005_create_daily_build_group_report_results.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class CreateDailyBuildGroupReportResults < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + create_table :ci_daily_build_group_report_results do |t| + t.date :date, null: false + t.bigint :project_id, null: false + t.bigint :last_pipeline_id, null: false + t.text :ref_path, null: false # rubocop:disable Migration/AddLimitToTextColumns + t.text :group_name, null: false # rubocop:disable Migration/AddLimitToTextColumns + t.jsonb :data, null: false + + t.index :last_pipeline_id + t.index [:project_id, :ref_path, :date, :group_name], name: 'index_daily_build_group_report_results_unique_columns', unique: true + t.foreign_key :projects, on_delete: :cascade + t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade + end + end +end diff --git a/db/structure.sql b/db/structure.sql index b5bc2d0d361..73edcc371ce 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1025,6 +1025,25 @@ CREATE SEQUENCE public.ci_builds_runner_session_id_seq ALTER SEQUENCE public.ci_builds_runner_session_id_seq OWNED BY public.ci_builds_runner_session.id; +CREATE TABLE public.ci_daily_build_group_report_results ( + id bigint NOT NULL, + date date NOT NULL, + project_id bigint NOT NULL, + last_pipeline_id bigint NOT NULL, + ref_path text NOT NULL, + group_name text NOT NULL, + data jsonb NOT NULL +); + +CREATE SEQUENCE public.ci_daily_build_group_report_results_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.ci_daily_build_group_report_results_id_seq OWNED BY public.ci_daily_build_group_report_results.id; + CREATE TABLE public.ci_daily_report_results ( id bigint NOT NULL, date date NOT NULL, @@ -7285,6 +7304,8 @@ ALTER TABLE ONLY public.ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval(' ALTER TABLE ONLY public.ci_builds_runner_session ALTER COLUMN id SET DEFAULT nextval('public.ci_builds_runner_session_id_seq'::regclass); +ALTER TABLE ONLY public.ci_daily_build_group_report_results ALTER COLUMN id SET DEFAULT nextval('public.ci_daily_build_group_report_results_id_seq'::regclass); + ALTER TABLE ONLY public.ci_daily_report_results ALTER COLUMN id SET DEFAULT nextval('public.ci_daily_report_results_id_seq'::regclass); ALTER TABLE ONLY public.ci_group_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_group_variables_id_seq'::regclass); @@ -7954,6 +7975,9 @@ ALTER TABLE ONLY public.ci_builds ALTER TABLE ONLY public.ci_builds_runner_session ADD CONSTRAINT ci_builds_runner_session_pkey PRIMARY KEY (id); +ALTER TABLE ONLY public.ci_daily_build_group_report_results + ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.ci_daily_report_results ADD CONSTRAINT ci_daily_report_results_pkey PRIMARY KEY (id); @@ -9148,6 +9172,8 @@ CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON pub CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id); +CREATE INDEX index_ci_daily_build_group_report_results_on_last_pipeline_id ON public.ci_daily_build_group_report_results USING btree (last_pipeline_id); + CREATE INDEX index_ci_daily_report_results_on_last_pipeline_id ON public.ci_daily_report_results USING btree (last_pipeline_id); CREATE UNIQUE INDEX index_ci_group_variables_on_group_id_and_key ON public.ci_group_variables USING btree (group_id, key); @@ -9356,6 +9382,8 @@ CREATE UNIQUE INDEX index_container_repositories_on_project_id_and_name ON publi CREATE INDEX index_container_repository_on_name_trigram ON public.container_repositories USING gin (name public.gin_trgm_ops); +CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON public.ci_daily_build_group_report_results USING btree (project_id, ref_path, date, group_name); + CREATE UNIQUE INDEX index_daily_report_results_unique_columns ON public.ci_daily_report_results USING btree (project_id, ref_path, param_type, date, title); CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON public.dependency_proxy_blobs USING btree (group_id, file_name); @@ -11469,6 +11497,9 @@ ALTER TABLE ONLY public.events ALTER TABLE ONLY public.ip_restrictions ADD CONSTRAINT fk_rails_04a93778d5 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.ci_daily_build_group_report_results + ADD CONSTRAINT fk_rails_0667f7608c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.ci_subscriptions_projects ADD CONSTRAINT fk_rails_0818751483 FOREIGN KEY (downstream_project_id) REFERENCES public.projects(id) ON DELETE CASCADE; @@ -12414,6 +12445,9 @@ ALTER TABLE ONLY public.ci_daily_report_results ALTER TABLE ONLY public.cluster_providers_aws ADD CONSTRAINT fk_rails_ed1fdfaeb2 FOREIGN KEY (created_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL; +ALTER TABLE ONLY public.ci_daily_build_group_report_results + ADD CONSTRAINT fk_rails_ee072d13b3 FOREIGN KEY (last_pipeline_id) REFERENCES public.ci_pipelines(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.label_priorities ADD CONSTRAINT fk_rails_ef916d14fa FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; @@ -13656,6 +13690,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200420172927 20200420201933 20200421092907 +20200421111005 20200421233150 20200422091541 20200422213749 diff --git a/doc/api/labels.md b/doc/api/labels.md index e3f367daaca..eb8ec906ec1 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -109,7 +109,7 @@ GET /projects/:id/labels/:label_id | Attribute | Type | Required | Description | | --------- | ------- | -------- | --------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `label_id` | integer or string | yes | The ID or title of a group's label. | +| `label_id` | integer or string | yes | The ID or title of a project's label. | | `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. | ```shell diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index e37c2ba3766..c7ecf1ab3d4 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -244,7 +244,7 @@ request: 1. The [CI environment preparation](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/prepare_build.sh). 1. The [Omnibus package creator](https://gitlab.com/gitlab-org/omnibus-gitlab). -### Incremental improvements +## Incremental improvements We allow engineering time to fix small problems (with or without an issue) that are incremental improvements, such as: diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 7e2d4b08767..83871f5c98e 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -280,6 +280,40 @@ const vm = mountComponent(Component, data); The main return value of a Vue component is the rendered output. In order to test the component we need to test the rendered output. [Vue](https://vuejs.org/v2/guide/unit-testing.html) guide's to unit test show us exactly that: +### Events + +We should test for events emitted in response to an action within our component, this is useful to verify the correct events are being fired with the correct arguments. + +For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) to fire out event. + +```javascript +// Assuming SomeButton renders: <button>Some button</button> +wrapper = mount(SomeButton); + +... +it('should fire the click event', () => { + const btn = wrapper.find('button') + + btn.trigger('click'); + ... +}) +``` + +When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html) to fire our event. + +```javascript +wrapper = shallowMount(DropdownItem); + +... + +it('should fire the itemClicked event', () => { + DropdownItem.vm.$emit('itemClicked'); + ... +}) +``` + +We should verify an event has been fired by asserting against the result of the [`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted) method + ## Vue.js Expert Role One should apply to be a Vue.js expert by opening an MR when the Merge Request's they create and review show: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f1c6e1569dd..1f29126109d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8395,7 +8395,7 @@ msgstr "" msgid "Error creating label." msgstr "" -msgid "Error deleting %{issuableType}" +msgid "Error deleting %{issuableType}" msgstr "" msgid "Error deleting project. Check logs for error details." @@ -72,6 +72,7 @@ module QA autoload :DeployKey, 'qa/resource/deploy_key' autoload :DeployToken, 'qa/resource/deploy_token' autoload :ProtectedBranch, 'qa/resource/protected_branch' + autoload :Pipeline, 'qa/resource/pipeline' autoload :CiVariable, 'qa/resource/ci_variable' autoload :Runner, 'qa/resource/runner' autoload :PersonalAccessToken, 'qa/resource/personal_access_token' diff --git a/qa/qa/resource/pipeline.rb b/qa/qa/resource/pipeline.rb new file mode 100644 index 00000000000..a115de3e825 --- /dev/null +++ b/qa/qa/resource/pipeline.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + module Resource + class Pipeline < Base + attribute :project do + Resource::Project.fabricate! do |project| + project.name = 'project-with-pipeline' + end + end + + attribute :id + attribute :status + attribute :ref + attribute :sha + + # array in form + # [ + # { key: 'UPLOAD_TO_S3', variable_type: 'file', value: true }, + # { key: 'SOMETHING', variable_type: 'env_var', value: 'yes' } + # ] + attribute :variables + + def initialize + @ref = 'master' + @variables = [] + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) + end + + def api_get_path + "/projects/#{project.id}/pipelines/#{id}" + end + + def api_post_path + "/projects/#{project.id}/pipeline" + end + + def api_post_body + { + ref: ref, + variables: variables + } + end + end + end +end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 1b671a076de..f41328c2d82 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -100,6 +100,10 @@ module QA "#{api_get_path}/runners" end + def api_pipelines_path + "#{api_get_path}/pipelines" + end + def api_put_path "/projects/#{id}" end @@ -161,6 +165,10 @@ module QA parse_body(response) end + def pipelines + parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url)) + end + def share_with_group(invitee, access_level = Resource::Members::AccessLevel::DEVELOPER) post Runtime::API::Request.new(api_client, "/projects/#{id}/share").url, { group_id: invitee.id, group_access: access_level } end diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb index 4a6ebc46311..361c7e10cdb 100644 --- a/spec/controllers/projects/alert_management_controller_spec.rb +++ b/spec/controllers/projects/alert_management_controller_spec.rb @@ -40,9 +40,9 @@ describe Projects::AlertManagementController do end describe 'GET #details' do - context 'when alert_management_minimal is enabled' do + context 'when alert_management_detail is enabled' do before do - stub_feature_flags(alert_management_minimal: true) + stub_feature_flags(alert_management_detail: true) end it 'shows the page' do @@ -52,9 +52,9 @@ describe Projects::AlertManagementController do end end - context 'when alert_management_minimal is disabled' do + context 'when alert_management_detail is disabled' do before do - stub_feature_flags(alert_management_minimal: false) + stub_feature_flags(alert_management_detail: false) end it 'shows 404' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 875371d26c9..26786aab12c 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -320,6 +320,12 @@ FactoryBot.define do end end + trait :accessibility_reports do + after(:build) do |build| + build.job_artifacts << create(:ci_job_artifact, :accessibility, job: build) + end + end + trait :coverage_reports do after(:build) do |build| build.job_artifacts << create(:ci_job_artifact, :cobertura, job: build) diff --git a/spec/factories/ci/daily_report_results.rb b/spec/factories/ci/daily_build_group_report_results.rb index e2255e8a134..7f72991b3eb 100644 --- a/spec/factories/ci/daily_report_results.rb +++ b/spec/factories/ci/daily_build_group_report_results.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true FactoryBot.define do - factory :ci_daily_report_result, class: 'Ci::DailyReportResult' do + factory :ci_daily_build_group_report_result, class: 'Ci::DailyBuildGroupReportResult' do ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' } date { Time.zone.now.to_date } project last_pipeline factory: :ci_pipeline - param_type { Ci::DailyReportResult.param_types[:coverage] } - title { 'rspec' } - value { 77.0 } + group_name { 'rspec' } + data do + { coverage: 77.0 } + end end end diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb new file mode 100644 index 00000000000..c3f17227701 --- /dev/null +++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Issues > Real-time sidebar', :js do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:user) { create(:user) } + + before_all do + project.add_developer(user) + end + + it 'updates the assignee in real-time' do + Capybara::Session.new(:other_session) + + using_session :other_session do + visit project_issue_path(project, issue) + expect(page.find('.assignee')).to have_content 'None' + end + + gitlab_sign_in(user) + visit project_issue_path(project, issue) + expect(page.find('.assignee')).to have_content 'None' + + click_button 'assign yourself' + + using_session :other_session do + expect(page.find('.assignee')).to have_content user.name + end + end +end diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js index 03279fc56a4..91bcef5cb62 100644 --- a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js +++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js @@ -10,6 +10,9 @@ export const Editor = { initialEditType: { type: String, }, + height: { + type: String, + }, }, render(h) { return h('div'); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 3b6021c8014..970bc99f8ff 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -44,6 +44,7 @@ import { setExpandedDiffLines, setSuggestPopoverDismissed, changeCurrentCommit, + moveToNeighboringCommit, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -1406,4 +1407,44 @@ describe('DiffsStoreActions', () => { }, ); }); + + describe('moveToNeighboringCommit', () => { + it.each` + direction | expected | currentCommit + ${'next'} | ${'NEXTSHA'} | ${{ next_commit_id: 'NEXTSHA' }} + ${'previous'} | ${'PREVIOUSSHA'} | ${{ prev_commit_id: 'PREVIOUSSHA' }} + `( + 'for the direction "$direction", dispatches the action to move to the SHA "$expected"', + ({ direction, expected, currentCommit }) => { + return testAction( + moveToNeighboringCommit, + { direction }, + { commit: currentCommit }, + [], + [{ type: 'changeCurrentCommit', payload: { commitId: expected } }], + ); + }, + ); + + it.each` + direction | diffsAreLoading | currentCommit + ${'next'} | ${false} | ${{ prev_commit_id: 'PREVIOUSSHA' }} + ${'next'} | ${true} | ${{ prev_commit_id: 'PREVIOUSSHA' }} + ${'next'} | ${false} | ${undefined} + ${'previous'} | ${false} | ${{ next_commit_id: 'NEXTSHA' }} + ${'previous'} | ${true} | ${{ next_commit_id: 'NEXTSHA' }} + ${'previous'} | ${false} | ${undefined} + `( + 'given `{ "isloading": $diffsAreLoading, "commit": $currentCommit }` in state, no actions are dispatched', + ({ direction, diffsAreLoading, currentCommit }) => { + return testAction( + moveToNeighboringCommit, + { direction }, + { commit: currentCommit, isLoading: diffsAreLoading }, + [], + [], + ); + }, + ); + }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js index 2d4d3ea28ff..42076e8da5c 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/frontend/issue_show/components/app_spec.js @@ -1,9 +1,8 @@ -/* eslint-disable no-unused-vars */ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; -import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; -import GLDropdown from '~/gl_dropdown'; +import { TEST_HOST } from 'helpers/test_constants'; import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; import '~/behaviors/markdown/render_gfm'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; @@ -13,6 +12,9 @@ function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); } +jest.mock('~/lib/utils/url_utility'); +jest.mock('~/issue_show/event_hub'); + const REALTIME_REQUEST_STACK = [initialRequest, secondRequest]; describe('Issuable output', () => { @@ -20,9 +22,10 @@ describe('Issuable output', () => { let realtimeRequestCount = 0; let vm; - beforeEach(done => { + beforeEach(() => { setFixtures(` <div> + <title>Title</title> <div class="detail-page-description content-block"> <details open> <summary>One</summary> @@ -35,7 +38,6 @@ describe('Issuable output', () => { <span id="task_status"></span> </div> `); - spyOn(eventHub, '$emit'); const IssuableDescriptionComponent = Vue.extend(issuableApp); @@ -53,7 +55,7 @@ describe('Issuable output', () => { canUpdate: true, canDestroy: true, endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes', - updateEndpoint: gl.TEST_HOST, + updateEndpoint: TEST_HOST, issuableRef: '#1', initialTitleHtml: '', initialTitleText: '', @@ -67,8 +69,6 @@ describe('Issuable output', () => { issuableTemplateNamesPath: '/issuable-templates-path', }, }).$mount(); - - setTimeout(done); }); afterEach(() => { @@ -79,9 +79,10 @@ describe('Issuable output', () => { vm.$destroy(); }); - it('should render a title/description/edited and update title/description/edited on update', done => { + it('should render a title/description/edited and update title/description/edited on update', () => { let editedText; - Vue.nextTick() + return axios + .waitForAll() .then(() => { editedText = vm.$el.querySelector('.edited-text'); }) @@ -100,8 +101,8 @@ describe('Issuable output', () => { }) .then(() => { vm.poll.makeRequest(); + return axios.waitForAll(); }) - .then(() => new Promise(resolve => setTimeout(resolve))) .then(() => { expect(document.querySelector('title').innerText).toContain('2 (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); @@ -115,312 +116,239 @@ describe('Issuable output', () => { expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/); expect(editedText.querySelector('time')).toBeTruthy(); expect(vm.state.lock_version).toEqual(2); - }) - .then(done) - .catch(done.fail); + }); }); - it('shows actions if permissions are correct', done => { + it('shows actions if permissions are correct', () => { vm.showForm = true; - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(vm.$el.querySelector('.btn')).not.toBeNull(); - - done(); }); }); - it('does not show actions if permissions are incorrect', done => { + it('does not show actions if permissions are incorrect', () => { vm.showForm = true; vm.canUpdate = false; - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(vm.$el.querySelector('.btn')).toBeNull(); - - done(); }); }); - it('does not update formState if form is already open', done => { + it('does not update formState if form is already open', () => { vm.updateAndShowForm(); vm.state.titleText = 'testing 123'; vm.updateAndShowForm(); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(vm.store.formState.title).not.toBe('testing 123'); + }); + }); + + it('opens reCAPTCHA modal if update rejected as spam', () => { + let modal; - done(); + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + }, }); + + vm.canUpdate = true; + vm.showForm = true; + + return vm + .$nextTick() + .then(() => { + vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc'; + return vm.updateIssuable(); + }) + .then(() => { + modal = vm.$el.querySelector('.js-recaptcha-modal'); + + expect(modal.style.display).not.toEqual('none'); + expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); + expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); + }) + .then(() => { + modal.querySelector('.close').click(); + return vm.$nextTick(); + }) + .then(() => { + expect(modal.style.display).toEqual('none'); + expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); + }); }); describe('updateIssuable', () => { - it('fetches new data after update', done => { - spyOn(vm, 'updateStoreState').and.callThrough(); - spyOn(vm.service, 'getData').and.callThrough(); - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { web_url: window.location.pathname }, - }), - ); - - vm.updateIssuable() - .then(() => { - expect(vm.updateStoreState).toHaveBeenCalled(); - expect(vm.service.getData).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + it('fetches new data after update', () => { + const updateStoreSpy = jest.spyOn(vm, 'updateStoreState'); + const getDataSpy = jest.spyOn(vm.service, 'getData'); + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { web_url: window.location.pathname }, + }); + + return vm.updateIssuable().then(() => { + expect(updateStoreSpy).toHaveBeenCalled(); + expect(getDataSpy).toHaveBeenCalled(); + }); }); - it('correctly updates issuable data', done => { - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { web_url: window.location.pathname }, - }), - ); + it('correctly updates issuable data', () => { + const spy = jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { web_url: window.location.pathname }, + }); - vm.updateIssuable() - .then(() => { - expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState); - expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); - }) - .then(done) - .catch(done.fail); + return vm.updateIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(vm.formState); + expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); + }); }); - it('does not redirect if issue has not moved', done => { - const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: window.location.pathname, - confidential: vm.isConfidential, - }, - }), - ); - - vm.updateIssuable(); + it('does not redirect if issue has not moved', () => { + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + web_url: window.location.pathname, + confidential: vm.isConfidential, + }, + }); - setTimeout(() => { + return vm.updateIssuable().then(() => { expect(visitUrl).not.toHaveBeenCalled(); - done(); }); }); - it('redirects if returned web_url has changed', done => { - const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.service, 'updateIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: '/testing-issue-move', - confidential: vm.isConfidential, - }, - }), - ); + it('redirects if returned web_url has changed', () => { + jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({ + data: { + web_url: '/testing-issue-move', + confidential: vm.isConfidential, + }, + }); vm.updateIssuable(); - setTimeout(() => { + return vm.updateIssuable().then(() => { expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move'); - done(); }); }); describe('shows dialog when issue has unsaved changed', () => { - it('confirms on title change', done => { + it('confirms on title change', () => { vm.showForm = true; vm.state.titleText = 'title has changed'; const e = { returnValue: null }; vm.handleBeforeUnloadEvent(e); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(e.returnValue).not.toBeNull(); - - done(); }); }); - it('confirms on description change', done => { + it('confirms on description change', () => { vm.showForm = true; vm.state.descriptionText = 'description has changed'; const e = { returnValue: null }; vm.handleBeforeUnloadEvent(e); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(e.returnValue).not.toBeNull(); - - done(); }); }); - it('does nothing when nothing has changed', done => { + it('does nothing when nothing has changed', () => { const e = { returnValue: null }; vm.handleBeforeUnloadEvent(e); - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(e.returnValue).toBeNull(); - - done(); }); }); }); describe('error when updating', () => { - it('closes form on error', done => { - spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject()); - vm.updateIssuable(); - - setTimeout(() => { + it('closes form on error', () => { + jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); + return vm.updateIssuable().then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `Error updating issue`, ); - - done(); }); }); - it('returns the correct error message for issuableType', done => { - spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject()); + it('returns the correct error message for issuableType', () => { + jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue(); vm.issuableType = 'merge request'; - Vue.nextTick(() => { - vm.updateIssuable(); - - setTimeout(() => { + return vm + .$nextTick() + .then(vm.updateIssuable) + .then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `Error updating merge request`, ); - - done(); }); - }); }); - it('shows error message from backend if exists', done => { + it('shows error message from backend if exists', () => { const msg = 'Custom error message from backend'; - spyOn(vm.service, 'updateIssuable').and.callFake( - // eslint-disable-next-line prefer-promise-reject-errors - () => Promise.reject({ response: { data: { errors: [msg] } } }), - ); + jest + .spyOn(vm.service, 'updateIssuable') + .mockRejectedValue({ response: { data: { errors: [msg] } } }); - vm.updateIssuable(); - setTimeout(() => { + return vm.updateIssuable().then(() => { expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( `${vm.defaultErrorMessage}. ${msg}`, ); - - done(); }); }); }); }); - it('opens reCAPTCHA modal if update rejected as spam', done => { - function mockScriptSrc() { - const recaptchaChild = vm.$children.find( - // eslint-disable-next-line no-underscore-dangle - child => child.$options._componentTag === 'recaptcha-modal', - ); - - recaptchaChild.scriptSrc = '//scriptsrc'; - } - - let modal; - const promise = new Promise(resolve => { - resolve({ + describe('deleteIssuable', () => { + it('changes URL when deleted', () => { + jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ data: { - recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + web_url: '/test', }, }); - }); - - spyOn(vm.service, 'updateIssuable').and.returnValue(promise); - - vm.canUpdate = true; - vm.showForm = true; - - vm.$nextTick() - .then(() => mockScriptSrc()) - .then(() => vm.updateIssuable()) - .then(promise) - .then(() => setTimeoutPromise()) - .then(() => { - modal = vm.$el.querySelector('.js-recaptcha-modal'); - expect(modal.style.display).not.toEqual('none'); - expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); - expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); - }) - .then(() => modal.querySelector('.close').click()) - .then(() => vm.$nextTick()) - .then(() => { - expect(modal.style.display).toEqual('none'); - expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - - describe('deleteIssuable', () => { - it('changes URL when deleted', done => { - const visitUrl = spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.service, 'deleteIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: '/test', - }, - }), - ); - - vm.deleteIssuable(); - - setTimeout(() => { + return vm.deleteIssuable().then(() => { expect(visitUrl).toHaveBeenCalledWith('/test'); - - done(); }); }); - it('stops polling when deleting', done => { - spyOnDependency(issuableApp, 'visitUrl'); - spyOn(vm.poll, 'stop').and.callThrough(); - spyOn(vm.service, 'deleteIssuable').and.returnValue( - Promise.resolve({ - data: { - web_url: '/test', - }, - }), - ); - - vm.deleteIssuable(); - - setTimeout(() => { - expect(vm.poll.stop).toHaveBeenCalledWith(); + it('stops polling when deleting', () => { + const spy = jest.spyOn(vm.poll, 'stop'); + jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({ + data: { + web_url: '/test', + }, + }); - done(); + return vm.deleteIssuable().then(() => { + expect(spy).toHaveBeenCalledWith(); }); }); - it('closes form on error', done => { - spyOn(vm.service, 'deleteIssuable').and.returnValue(Promise.reject()); + it('closes form on error', () => { + jest.spyOn(vm.service, 'deleteIssuable').mockRejectedValue(); - vm.deleteIssuable(); - - setTimeout(() => { + return vm.deleteIssuable().then(() => { expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form'); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( 'Error deleting issue', ); - - done(); }); }); }); describe('updateAndShowForm', () => { - it('shows locked warning if form is open & data is different', done => { - vm.$nextTick() + it('shows locked warning if form is open & data is different', () => { + return vm + .$nextTick() .then(() => { vm.updateAndShowForm(); @@ -436,44 +364,38 @@ describe('Issuable output', () => { expect(vm.formState.lockedWarningVisible).toEqual(true); expect(vm.formState.lock_version).toEqual(1); expect(vm.$el.querySelector('.alert')).not.toBeNull(); - }) - .then(done) - .catch(done.fail); + }); }); }); describe('requestTemplatesAndShowForm', () => { + let formSpy; + beforeEach(() => { - spyOn(vm, 'updateAndShowForm'); + formSpy = jest.spyOn(vm, 'updateAndShowForm'); }); - it('shows the form if template names request is successful', done => { + it('shows the form if template names request is successful', () => { const mockData = [{ name: 'Bug' }]; mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData])); - vm.requestTemplatesAndShowForm() - .then(() => { - expect(vm.updateAndShowForm).toHaveBeenCalledWith(mockData); - }) - .then(done) - .catch(done.fail); + return vm.requestTemplatesAndShowForm().then(() => { + expect(formSpy).toHaveBeenCalledWith(mockData); + }); }); - it('shows the form if template names request failed', done => { + it('shows the form if template names request failed', () => { mock .onGet('/issuable-templates-path') .reply(() => Promise.reject(new Error('something went wrong'))); - vm.requestTemplatesAndShowForm() - .then(() => { - expect(document.querySelector('.flash-container .flash-text').textContent).toContain( - 'Error updating issue', - ); + return vm.requestTemplatesAndShowForm().then(() => { + expect(document.querySelector('.flash-container .flash-text').textContent).toContain( + 'Error updating issue', + ); - expect(vm.updateAndShowForm).toHaveBeenCalledWith(); - }) - .then(done) - .catch(done.fail); + expect(formSpy).toHaveBeenCalledWith(); + }); }); }); @@ -490,32 +412,26 @@ describe('Issuable output', () => { }); describe('updateStoreState', () => { - it('should make a request and update the state of the store', done => { + it('should make a request and update the state of the store', () => { const data = { foo: 1 }; - spyOn(vm.store, 'updateState'); - spyOn(vm.service, 'getData').and.returnValue(Promise.resolve({ data })); + const getDataSpy = jest.spyOn(vm.service, 'getData').mockResolvedValue({ data }); + const updateStateSpy = jest.spyOn(vm.store, 'updateState').mockImplementation(jest.fn); - vm.updateStoreState() - .then(() => { - expect(vm.service.getData).toHaveBeenCalled(); - expect(vm.store.updateState).toHaveBeenCalledWith(data); - }) - .then(done) - .catch(done.fail); + return vm.updateStoreState().then(() => { + expect(getDataSpy).toHaveBeenCalled(); + expect(updateStateSpy).toHaveBeenCalledWith(data); + }); }); - it('should show error message if store update fails', done => { - spyOn(vm.service, 'getData').and.returnValue(Promise.reject()); + it('should show error message if store update fails', () => { + jest.spyOn(vm.service, 'getData').mockRejectedValue(); vm.issuableType = 'merge request'; - vm.updateStoreState() - .then(() => { - expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( - `Error updating ${vm.issuableType}`, - ); - }) - .then(done) - .catch(done.fail); + return vm.updateStoreState().then(() => { + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + `Error updating ${vm.issuableType}`, + ); + }); }); }); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js index 194f177d837..9c448c498e2 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/frontend/issue_show/components/description_spec.js @@ -1,8 +1,12 @@ import $ from 'jquery'; import Vue from 'vue'; import '~/behaviors/markdown/render_gfm'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import Description from '~/issue_show/components/description.vue'; +import TaskList from '~/task_list'; + +jest.mock('~/task_list'); describe('Description component', () => { let vm; @@ -13,7 +17,7 @@ describe('Description component', () => { descriptionText: 'test', updatedAt: new Date().toString(), taskStatus: '', - updateUrl: gl.TEST_HOST, + updateUrl: TEST_HOST, }; beforeEach(() => { @@ -39,25 +43,26 @@ describe('Description component', () => { $('.issuable-meta .flash-container').remove(); }); - it('animates description changes', done => { + it('animates description changes', () => { vm.descriptionHtml = 'changed'; - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), - ).toBeTruthy(); - - setTimeout(() => { + return vm + .$nextTick() + .then(() => { + expect( + vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), + ).toBeTruthy(); + jest.runAllTimers(); + return vm.$nextTick(); + }) + .then(() => { expect( vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'), ).toBeTruthy(); - - done(); }); - }); }); - it('opens reCAPTCHA dialog if update rejected as spam', done => { + it('opens reCAPTCHA dialog if update rejected as spam', () => { let modal; const recaptchaChild = vm.$children.find( // eslint-disable-next-line no-underscore-dangle @@ -70,7 +75,8 @@ describe('Description component', () => { recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', }); - vm.$nextTick() + return vm + .$nextTick() .then(() => { modal = vm.$el.querySelector('.js-recaptcha-modal'); @@ -83,128 +89,105 @@ describe('Description component', () => { .then(() => { expect(modal.style.display).toEqual('none'); expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); - }) - .then(done) - .catch(done.fail); + }); }); - describe('TaskList', () => { - let TaskList; + it('applies syntax highlighting and math when description changed', () => { + const vmSpy = jest.spyOn(vm, 'renderGFM'); + const prototypeSpy = jest.spyOn($.prototype, 'renderGFM'); + vm.descriptionHtml = 'changed'; + return vm.$nextTick().then(() => { + expect(vm.$refs['gfm-content']).toBeDefined(); + expect(vmSpy).toHaveBeenCalled(); + expect(prototypeSpy).toHaveBeenCalled(); + expect($.prototype.renderGFM).toHaveBeenCalled(); + }); + }); + + it('sets data-update-url', () => { + expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST); + }); + + describe('TaskList', () => { beforeEach(() => { vm.$destroy(); + TaskList.mockClear(); vm = mountComponent( DescriptionComponent, Object.assign({}, props, { issuableType: 'issuableType', }), ); - TaskList = spyOnDependency(Description, 'TaskList'); }); - it('re-inits the TaskList when description changed', done => { + it('re-inits the TaskList when description changed', () => { vm.descriptionHtml = 'changed'; - setTimeout(() => { - expect(TaskList).toHaveBeenCalled(); - done(); - }); + expect(TaskList).toHaveBeenCalled(); }); - it('does not re-init the TaskList when canUpdate is false', done => { + it('does not re-init the TaskList when canUpdate is false', () => { vm.canUpdate = false; vm.descriptionHtml = 'changed'; - setTimeout(() => { - expect(TaskList).not.toHaveBeenCalled(); - done(); - }); + expect(TaskList).toHaveBeenCalledTimes(1); }); - it('calls with issuableType dataType', done => { + it('calls with issuableType dataType', () => { vm.descriptionHtml = 'changed'; - setTimeout(() => { - expect(TaskList).toHaveBeenCalledWith({ - dataType: 'issuableType', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: jasmine.any(Function), - onError: jasmine.any(Function), - lockVersion: 0, - }); - - done(); + expect(TaskList).toHaveBeenCalledWith({ + dataType: 'issuableType', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: expect.any(Function), + onError: expect.any(Function), + lockVersion: 0, }); }); }); describe('taskStatus', () => { - it('adds full taskStatus', done => { + it('adds full taskStatus', () => { vm.taskStatus = '1 of 1'; - setTimeout(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe( '1 of 1', ); - - done(); }); }); - it('adds short taskStatus', done => { + it('adds short taskStatus', () => { vm.taskStatus = '1 of 1'; - setTimeout(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe( '1/1 task', ); - - done(); }); }); - it('clears task status text when no tasks are present', done => { + it('clears task status text when no tasks are present', () => { vm.taskStatus = '0 of 0'; - setTimeout(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(''); - - done(); }); }); }); - it('applies syntax highlighting and math when description changed', done => { - spyOn(vm, 'renderGFM').and.callThrough(); - spyOn($.prototype, 'renderGFM').and.callThrough(); - vm.descriptionHtml = 'changed'; - - Vue.nextTick(() => { - setTimeout(() => { - expect(vm.$refs['gfm-content']).toBeDefined(); - expect(vm.renderGFM).toHaveBeenCalled(); - expect($.prototype.renderGFM).toHaveBeenCalled(); - - done(); - }); - }); - }); - - it('sets data-update-url', () => { - expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST); - }); - describe('taskListUpdateError', () => { it('should create flash notification and emit an event to parent', () => { const msg = 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.'; - spyOn(vm, '$emit'); + const spy = jest.spyOn(vm, '$emit'); vm.taskListUpdateError(); expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg); - expect(vm.$emit).toHaveBeenCalledWith('taskListUpdateFailed'); + expect(spy).toHaveBeenCalledWith('taskListUpdateFailed'); }); }); }); diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/frontend/issue_show/components/edited_spec.js index a1683f060c0..a1683f060c0 100644 --- a/spec/javascripts/issue_show/components/edited_spec.js +++ b/spec/frontend/issue_show/components/edited_spec.js diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js index 8d77a620d76..9ebab31f1ad 100644 --- a/spec/javascripts/issue_show/components/fields/description_template_spec.js +++ b/spec/frontend/issue_show/components/fields/description_template_spec.js @@ -5,7 +5,7 @@ describe('Issue description template component', () => { let vm; let formState; - beforeEach(done => { + beforeEach(() => { const Component = Vue.extend(descriptionTemplate); formState = { description: 'test', @@ -19,8 +19,6 @@ describe('Issue description template component', () => { projectNamespace: '/', }, }).$mount(); - - Vue.nextTick(done); }); it('renders templates as JSON array in data attribute', () => { diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js index a111333ac80..b06a3a89d3b 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/frontend/issue_show/components/form_spec.js @@ -1,8 +1,11 @@ import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent from 'helpers/vue_mount_component_helper'; import formComponent from '~/issue_show/components/form.vue'; +import Autosave from '~/autosave'; import eventHub from '~/issue_show/event_hub'; +jest.mock('~/autosave'); + describe('Inline edit form component', () => { let vm; const defaultProps = { @@ -65,18 +68,16 @@ describe('Inline edit form component', () => { }); describe('autosave', () => { - let autosaveObj; - let autosave; + let spy; beforeEach(() => { - autosaveObj = { reset: jasmine.createSpy() }; - autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj); + spy = jest.spyOn(Autosave.prototype, 'reset'); }); it('initialized Autosave on mount', () => { createComponent(); - expect(autosave).toHaveBeenCalledTimes(2); + expect(Autosave).toHaveBeenCalledTimes(2); }); it('calls reset on autosave when eventHub emits appropriate events', () => { @@ -84,15 +85,15 @@ describe('Inline edit form component', () => { eventHub.$emit('close.form'); - expect(autosaveObj.reset).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(2); eventHub.$emit('delete.issuable'); - expect(autosaveObj.reset).toHaveBeenCalledTimes(4); + expect(spy).toHaveBeenCalledTimes(4); eventHub.$emit('update.issuable'); - expect(autosaveObj.reset).toHaveBeenCalledTimes(6); + expect(spy).toHaveBeenCalledTimes(6); }); }); }); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/frontend/issue_show/components/title_spec.js index 9754c8a6755..c274048fdd5 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/frontend/issue_show/components/title_spec.js @@ -5,8 +5,9 @@ import eventHub from '~/issue_show/event_hub'; describe('Title component', () => { let vm; - beforeEach(() => { + setFixtures(`<title />`); + const Component = Vue.extend(titleComponent); const store = new Store({ titleHtml: '', @@ -28,51 +29,39 @@ describe('Title component', () => { expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>'); }); - it('updates page title when changing titleHtml', done => { - spyOn(vm, 'setPageTitle'); + it('updates page title when changing titleHtml', () => { + const spy = jest.spyOn(vm, 'setPageTitle'); vm.titleHtml = 'test'; - Vue.nextTick(() => { - expect(vm.setPageTitle).toHaveBeenCalled(); - - done(); + return vm.$nextTick().then(() => { + expect(spy).toHaveBeenCalled(); }); }); - it('animates title changes', done => { + it('animates title changes', () => { vm.titleHtml = 'test'; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'), - ).toBeTruthy(); - - setTimeout(() => { - expect( - vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'), - ).toBeTruthy(); - - done(); + return vm + .$nextTick() + .then(() => { + expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse'); + jest.runAllTimers(); + return vm.$nextTick(); + }) + .then(() => { + expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse'); }); - }); }); - it('updates page title after changing title', done => { + it('updates page title after changing title', () => { vm.titleHtml = 'changed'; vm.titleText = 'changed'; - Vue.nextTick(() => { + return vm.$nextTick().then(() => { expect(document.querySelector('title').textContent.trim()).toContain('changed'); - - done(); }); }); describe('inline edit button', () => { - beforeEach(() => { - spyOn(eventHub, '$emit'); - }); - it('should not show by default', () => { expect(vm.$el.querySelector('.btn-edit')).toBeNull(); }); @@ -92,6 +81,7 @@ describe('Title component', () => { }); it('should trigger open.form event when clicked', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); vm.showInlineEditButton = true; vm.canUpdate = true; diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js index 82e39447ae6..ca03abfe6bb 100644 --- a/spec/frontend/static_site_editor/pages/home_spec.js +++ b/spec/frontend/static_site_editor/pages/home_spec.js @@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '@gitlab/ui'; import createState from '~/static_site_editor/store/state'; import Home from '~/static_site_editor/pages/home.vue'; -import EditArea from '~/static_site_editor/components/edit_area.vue'; +import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import EditHeader from '~/static_site_editor/components/edit_header.vue'; import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; @@ -71,10 +71,13 @@ describe('static_site_editor/pages/home', () => { wrapper = shallowMount(Home, { localVue, store, + provide: { + glFeatures: { richContentEditor: true }, + }, }); }; - const findEditArea = () => wrapper.find(EditArea); + const findRichContentEditor = () => wrapper.find(RichContentEditor); const findEditHeader = () => wrapper.find(EditHeader); const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); const findPublishToolbar = () => wrapper.find(PublishToolbar); @@ -103,8 +106,8 @@ describe('static_site_editor/pages/home', () => { }); describe('when content is not loaded', () => { - it('does not render edit area', () => { - expect(findEditArea().exists()).toBe(false); + it('does not render rich content editor', () => { + expect(findRichContentEditor().exists()).toBe(false); }); it('does not render edit header', () => { @@ -129,8 +132,8 @@ describe('static_site_editor/pages/home', () => { buildWrapper(); }); - it('renders the edit area', () => { - expect(findEditArea().exists()).toBe(true); + it('renders the rich content editor', () => { + expect(findRichContentEditor().exists()).toBe(true); }); it('renders the edit header', () => { @@ -141,8 +144,8 @@ describe('static_site_editor/pages/home', () => { expect(findSkeletonLoader().exists()).toBe(false); }); - it('passes page content to edit area', () => { - expect(findEditArea().props('value')).toBe(content); + it('passes page content to the rich content editor', () => { + expect(findRichContentEditor().props('value')).toBe(content); }); it('passes page title to edit header', () => { @@ -228,11 +231,11 @@ describe('static_site_editor/pages/home', () => { expect(loadContentActionMock).toHaveBeenCalled(); }); - it('dispatches setContent action when edit area emits input event', () => { + it('dispatches setContent action when rich content editor emits input event', () => { buildContentLoadedStore(); buildWrapper(); - findEditArea().vm.$emit('input', sourceContent); + findRichContentEditor().vm.$emit('input', sourceContent); expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined); }); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js index 933609c3072..774fe25387a 100644 --- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -50,6 +50,10 @@ describe('Rich Content Editor', () => { it('has the correct initial edit type', () => { expect(findEditor().props().initialEditType).toBe('wysiwyg'); }); + + it('has the correct height', () => { + expect(findEditor().props().height).toBe('100%'); + }); }); describe('when content is changed', () => { diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js deleted file mode 100644 index 951acfd4e10..00000000000 --- a/spec/javascripts/issue_show/helpers.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/issue_show/helpers.js'; diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js deleted file mode 100644 index 1b391bd1588..00000000000 --- a/spec/javascripts/issue_show/mock_data.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/issue_show/mock_data'; diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index d2790a6b858..03930c6c1a7 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -217,7 +217,7 @@ ci_pipelines: - vulnerability_findings - pipeline_config - security_scans -- daily_report_results +- daily_build_group_report_results pipeline_variables: - pipeline stages: @@ -484,7 +484,7 @@ project: - status_page_setting - requirements - export_jobs -- daily_report_results +- daily_build_group_report_results - jira_imports - compliance_framework_setting - metrics_users_starred_dashboards diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 266d39a0dfb..3a608391b2b 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3834,6 +3834,61 @@ describe Ci::Build do end end + describe '#collect_accessibility_reports!' do + subject { build.collect_accessibility_reports!(accessibility_report) } + + let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + + it { expect(accessibility_report.urls).to eq({}) } + + context 'when build has an accessibility report' do + context 'when there is an accessibility report with errors' do + before do + create(:ci_job_artifact, :accessibility, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to match_array(['https://about.gitlab.com/']) + expect(accessibility_report.errors_count).to eq(10) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.passes_count).to eq(0) + end + end + + context 'when there is an accessibility report without errors' do + before do + create(:ci_job_artifact, :accessibility_without_errors, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to match_array(['https://pa11y.org/']) + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.passes_count).to eq(1) + end + end + + context 'when there is an accessibility report with an invalid url' do + before do + create(:ci_job_artifact, :accessibility_with_invalid_url, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls).to be_empty + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.scans_count).to eq(0) + expect(accessibility_report.passes_count).to eq(0) + end + end + end + end + describe '#collect_coverage_reports!' do subject { build.collect_coverage_reports!(coverage_report) } diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index 61aa58c6692..d4c305c649a 100644 --- a/spec/models/ci/daily_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -describe Ci::DailyReportResult do +describe Ci::DailyBuildGroupReportResult do describe '.upsert_reports' do let!(:rspec_coverage) do create( - :ci_daily_report_result, - title: 'rspec', + :ci_daily_build_group_report_result, + group_name: 'rspec', date: '2020-03-09', - value: 71.2 + data: { coverage: 71.2 } ) end let!(:new_pipeline) { create(:ci_pipeline) } @@ -19,20 +19,18 @@ describe Ci::DailyReportResult do { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: described_class.param_types[rspec_coverage.param_type], last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - title: 'rspec', - value: 81.0 + group_name: 'rspec', + data: { 'coverage' => 81.0 } }, { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: described_class.param_types[rspec_coverage.param_type], last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - title: 'karma', - value: 87.0 + group_name: 'karma', + data: { 'coverage' => 87.0 } } ]) @@ -40,16 +38,15 @@ describe Ci::DailyReportResult do expect(rspec_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: 81.0 + data: { 'coverage' => 81.0 } ) - expect(described_class.find_by_title('karma')).to have_attributes( + expect(described_class.find_by_group_name('karma')).to have_attributes( project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: rspec_coverage.param_type, last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - value: 87.0 + data: { 'coverage' => 87.0 } ) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3df87e5d2b4..b8e10f43ef4 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1163,8 +1163,8 @@ describe Ci::Pipeline, :mailer do context "from #{status}" do let(:from_status) { status } - it 'schedules pipeline success worker' do - expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id) + it 'schedules daily build group report results worker' do + expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id) pipeline.succeed end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 13f1bcb389a..5a9ca9f7b7e 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -655,26 +655,4 @@ describe GroupPolicy do end end end - - it_behaves_like 'model with wiki policies' do - let(:container) { create(:group) } - - def set_access_level(access_level) - allow(container).to receive(:wiki_access_level).and_return(access_level) - end - - before do - stub_feature_flags(group_wiki: true) - end - - context 'when the feature flag is disabled' do - before do - stub_feature_flags(group_wiki: false) - end - - it 'does not include the wiki permissions' do - expect_disallowed(*permissions) - end - end - end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index f214b1ccf17..4e15af7e0b5 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -124,6 +124,7 @@ describe ProjectPolicy do it_behaves_like 'model with wiki policies' do let(:container) { project } + let_it_be(:user) { owner } def set_access_level(access_level) project.project_feature.update_attribute(:wiki_access_level, access_level) diff --git a/spec/services/ci/daily_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb index 240709bab0b..f0b72b8fd86 100644 --- a/spec/services/ci/daily_report_result_service_spec.rb +++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Ci::DailyReportResultService, '#execute' do +describe Ci::DailyBuildGroupReportResultService, '#execute' do let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') } let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) } let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) } @@ -11,31 +11,29 @@ describe Ci::DailyReportResultService, '#execute' do it 'creates daily code coverage record for each job in the pipeline that has coverage value' do described_class.new.execute(pipeline) - Ci::DailyReportResult.find_by(title: 'rspec').tap do |coverage| + Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec').tap do |coverage| expect(coverage).to have_attributes( project_id: pipeline.project.id, last_pipeline_id: pipeline.id, ref_path: pipeline.source_ref_path, - param_type: 'coverage', - title: rspec_job.group_name, - value: rspec_job.coverage, + group_name: rspec_job.group_name, + data: { 'coverage' => rspec_job.coverage }, date: pipeline.created_at.to_date ) end - Ci::DailyReportResult.find_by(title: 'karma').tap do |coverage| + Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma').tap do |coverage| expect(coverage).to have_attributes( project_id: pipeline.project.id, last_pipeline_id: pipeline.id, ref_path: pipeline.source_ref_path, - param_type: 'coverage', - title: karma_job.group_name, - value: karma_job.coverage, + group_name: karma_job.group_name, + data: { 'coverage' => karma_job.coverage }, date: pipeline.created_at.to_date ) end - expect(Ci::DailyReportResult.find_by(title: 'extra')).to be_nil + expect(Ci::DailyBuildGroupReportResult.find_by(group_name: 'extra')).to be_nil end context 'when there are multiple builds with the same group name that report coverage' do @@ -45,14 +43,13 @@ describe Ci::DailyReportResultService, '#execute' do it 'creates daily code coverage record with the average as the value' do described_class.new.execute(pipeline) - Ci::DailyReportResult.find_by(title: 'test').tap do |coverage| + Ci::DailyBuildGroupReportResult.find_by(group_name: 'test').tap do |coverage| expect(coverage).to have_attributes( project_id: pipeline.project.id, last_pipeline_id: pipeline.id, ref_path: pipeline.source_ref_path, - param_type: 'coverage', - title: test_job_2.group_name, - value: 75, + group_name: test_job_2.group_name, + data: { 'coverage' => 75.0 }, date: pipeline.created_at.to_date ) end @@ -77,8 +74,8 @@ describe Ci::DailyReportResultService, '#execute' do end it "updates the existing record's coverage value and last_pipeline_id" do - rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec') - karma_coverage = Ci::DailyReportResult.find_by(title: 'karma') + rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec') + karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma') # Bump up the coverage values described_class.new.execute(new_pipeline) @@ -88,12 +85,12 @@ describe Ci::DailyReportResultService, '#execute' do expect(rspec_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: new_rspec_job.coverage + data: { 'coverage' => new_rspec_job.coverage } ) expect(karma_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: new_karma_job.coverage + data: { 'coverage' => new_karma_job.coverage } ) end end @@ -117,8 +114,8 @@ describe Ci::DailyReportResultService, '#execute' do end it 'updates the existing daily code coverage records' do - rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec') - karma_coverage = Ci::DailyReportResult.find_by(title: 'karma') + rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec') + karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma') # Run another one but for the older pipeline. # This simulates the scenario wherein the success worker @@ -135,12 +132,12 @@ describe Ci::DailyReportResultService, '#execute' do expect(rspec_coverage).to have_attributes( last_pipeline_id: pipeline.id, - value: rspec_job.coverage + data: { 'coverage' => rspec_job.coverage } ) expect(karma_coverage).to have_attributes( last_pipeline_id: pipeline.id, - value: karma_job.coverage + data: { 'coverage' => karma_job.coverage } ) end end diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb index f77b5a2e5b9..e9e356ab4f6 100644 --- a/spec/services/groups/import_export/export_service_spec.rb +++ b/spec/services/groups/import_export/export_service_spec.rb @@ -50,7 +50,7 @@ describe Groups::ImportExport::ExportService do end it 'saves the models using ndjson tree saver' do - stub_feature_flags(group_import_export_ndjson: true) + stub_feature_flags(group_export_ndjson: true) expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original @@ -58,7 +58,7 @@ describe Groups::ImportExport::ExportService do end it 'saves the models using legacy tree saver' do - stub_feature_flags(group_import_export_ndjson: false) + stub_feature_flags(group_export_ndjson: false) expect(Gitlab::ImportExport::Group::LegacyTreeSaver).to receive(:new).and_call_original diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb index cd7ad1a1cfa..256e0a1b3c5 100644 --- a/spec/services/groups/import_export/import_service_spec.rb +++ b/spec/services/groups/import_export/import_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Groups::ImportExport::ImportService do - context 'with group_import_export_ndjson feature flag disabled' do + context 'with group_import_ndjson feature flag disabled' do let(:user) { create(:admin) } let(:group) { create(:group) } let(:import_logger) { instance_double(Gitlab::Import::Logger) } @@ -11,7 +11,7 @@ describe Groups::ImportExport::ImportService do subject(:service) { described_class.new(group: group, user: user) } before do - stub_feature_flags(group_import_export_ndjson: false) + stub_feature_flags(group_import_ndjson: false) ImportExportUpload.create(group: group, import_file: import_file) @@ -39,9 +39,9 @@ describe Groups::ImportExport::ImportService do end end - context 'with group_import_export_ndjson feature flag enabled' do + context 'with group_import_ndjson feature flag enabled' do before do - stub_feature_flags(group_import_export_ndjson: true) + stub_feature_flags(group_import_ndjson: true) end context 'when importing a ndjson export' do diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index de64cea6474..a0d54666dff 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -14,17 +14,16 @@ RSpec.shared_context 'GroupPolicy context' do %i[ read_label read_group upload_file read_namespace read_group_activity read_group_issues read_group_boards read_group_labels read_group_milestones - read_group_merge_requests read_wiki + read_group_merge_requests ] end let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } - let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation download_wiki_code] } - let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] } + let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] } + let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } let(:maintainer_permissions) do %i[ create_projects read_cluster create_cluster update_cluster admin_cluster add_cluster - admin_wiki ] end let(:owner_permissions) do diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb index b91500ffd9c..bd9e3a26f1e 100644 --- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb +++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb @@ -1,152 +1,114 @@ # frozen_string_literal: true RSpec.shared_examples 'model with wiki policies' do - let(:container) { raise NotImplementedError } - let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) } - - # TODO: Remove this helper once we implement group features - # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 - def set_access_level(access_level) - raise NotImplementedError - end - - subject { described_class.new(owner, container) } - - context 'when the feature is disabled' do - before do - set_access_level(ProjectFeature::DISABLED) - end + include ProjectHelpers - it 'does not include the wiki permissions' do - expect_disallowed(*permissions) - end + let(:container) { raise NotImplementedError } + let(:user) { raise NotImplementedError } - context 'when there is an external wiki' do - it 'does not include the wiki permissions' do - allow(container).to receive(:has_external_wiki?).and_return(true) + subject { described_class.new(user, container) } - expect_disallowed(*permissions) - end + let_it_be(:wiki_permissions) do + {}.tap do |permissions| + permissions[:guest] = %i[read_wiki] + permissions[:reporter] = permissions[:guest] + %i[download_wiki_code] + permissions[:developer] = permissions[:reporter] + %i[create_wiki] + permissions[:maintainer] = permissions[:developer] + %i[admin_wiki] + permissions[:all] = permissions[:maintainer] end end - describe 'read_wiki' do - subject { described_class.new(user, container) } - - member_roles = %i[guest developer] - stranger_roles = %i[anonymous non_member] - - user_roles = stranger_roles + member_roles + using RSpec::Parameterized::TableSyntax + + where(:container_level, :access_level, :membership, :access) do + :public | :enabled | :admin | :all + :public | :enabled | :maintainer | :maintainer + :public | :enabled | :developer | :developer + :public | :enabled | :reporter | :reporter + :public | :enabled | :guest | :guest + :public | :enabled | :non_member | :guest + :public | :enabled | :anonymous | :guest + + :public | :private | :admin | :all + :public | :private | :maintainer | :maintainer + :public | :private | :developer | :developer + :public | :private | :reporter | :reporter + :public | :private | :guest | :guest + :public | :private | :non_member | nil + :public | :private | :anonymous | nil + + :public | :disabled | :admin | nil + :public | :disabled | :maintainer | nil + :public | :disabled | :developer | nil + :public | :disabled | :reporter | nil + :public | :disabled | :guest | nil + :public | :disabled | :non_member | nil + :public | :disabled | :anonymous | nil + + :internal | :enabled | :admin | :all + :internal | :enabled | :maintainer | :maintainer + :internal | :enabled | :developer | :developer + :internal | :enabled | :reporter | :reporter + :internal | :enabled | :guest | :guest + :internal | :enabled | :non_member | :guest + :internal | :enabled | :anonymous | nil + + :internal | :private | :admin | :all + :internal | :private | :maintainer | :maintainer + :internal | :private | :developer | :developer + :internal | :private | :reporter | :reporter + :internal | :private | :guest | :guest + :internal | :private | :non_member | nil + :internal | :private | :anonymous | nil + + :internal | :disabled | :admin | nil + :internal | :disabled | :maintainer | nil + :internal | :disabled | :developer | nil + :internal | :disabled | :reporter | nil + :internal | :disabled | :guest | nil + :internal | :disabled | :non_member | nil + :internal | :disabled | :anonymous | nil + + :private | :private | :admin | :all + :private | :private | :maintainer | :maintainer + :private | :private | :developer | :developer + :private | :private | :reporter | :reporter + :private | :private | :guest | :guest + :private | :private | :non_member | nil + :private | :private | :anonymous | nil + + :private | :disabled | :admin | nil + :private | :disabled | :maintainer | nil + :private | :disabled | :developer | nil + :private | :disabled | :reporter | nil + :private | :disabled | :guest | nil + :private | :disabled | :non_member | nil + :private | :disabled | :anonymous | nil + end - # When a user is anonymous, their `current_user == nil` - let(:user) { create(:user) unless user_role == :anonymous } + with_them do + let(:user) { create_user_from_membership(container, membership) } + let(:allowed_permissions) { wiki_permissions[access].dup || [] } + let(:disallowed_permissions) { wiki_permissions[:all] - allowed_permissions } before do - container.visibility = container_visibility - set_access_level(wiki_access_level) - container.add_user(user, user_role) if member_roles.include?(user_role) - end - - title = ->(container_visibility, wiki_access_level, user_role) do - [ - "container is #{Gitlab::VisibilityLevel.level_name container_visibility}", - "wiki is #{ProjectFeature.str_from_access_level wiki_access_level}", - "user is #{user_role}" - ].join(', ') - end - - describe 'Situations where :read_wiki is always false' do - where(case_names: title, - container_visibility: Gitlab::VisibilityLevel.options.values, - wiki_access_level: [ProjectFeature::DISABLED], - user_role: user_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - - describe 'Situations where :read_wiki is always true' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: user_roles) + container.visibility = container_level.to_s + set_access_level(ProjectFeature.access_level_from_str(access_level.to_s)) - with_them do - it { is_expected.to be_allowed(:read_wiki) } + if allowed_permissions.any? && [container_level, access_level, membership] != [:private, :private, :guest] + allowed_permissions << :download_wiki_code end end - describe 'Situations where :read_wiki requires membership' do - context 'the wiki is private, and the user is a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC, - Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::PRIVATE], - user_role: member_roles) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the wiki is private, and the user is not member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PUBLIC, - Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::PRIVATE], - user_role: stranger_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - - context 'the wiki is enabled, and the user is a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PRIVATE], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: member_roles) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the wiki is enabled, and the user is not a member' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::PRIVATE], - wiki_access_level: [ProjectFeature::ENABLED], - user_role: stranger_roles) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end + it 'allows actions based on membership' do + expect_allowed(*allowed_permissions) + expect_disallowed(*disallowed_permissions) end + end - describe 'Situations where :read_wiki prohibits anonymous access' do - context 'the user is not anonymous' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC], - user_role: user_roles.reject { |u| u == :anonymous }) - - with_them do - it { is_expected.to be_allowed(:read_wiki) } - end - end - - context 'the user is anonymous' do - where(case_names: title, - container_visibility: [Gitlab::VisibilityLevel::INTERNAL], - wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC], - user_role: %i[anonymous]) - - with_them do - it { is_expected.to be_disallowed(:read_wiki) } - end - end - end + # TODO: Remove this helper once we implement group features + # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 + def set_access_level(access_level) + raise NotImplementedError end end diff --git a/spec/workers/ci/daily_report_results_worker_spec.rb b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb index b6543b32b09..d9706982a62 100644 --- a/spec/workers/ci/daily_report_results_worker_spec.rb +++ b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Ci::DailyReportResultsWorker do +describe Ci::DailyBuildGroupReportResultsWorker do describe '#perform' do let!(:pipeline) { create(:ci_pipeline) } @@ -12,7 +12,7 @@ describe Ci::DailyReportResultsWorker do let(:pipeline_id) { pipeline.id } it 'executes service' do - expect_any_instance_of(Ci::DailyReportResultService) + expect_any_instance_of(Ci::DailyBuildGroupReportResultService) .to receive(:execute).with(pipeline) subject @@ -23,7 +23,7 @@ describe Ci::DailyReportResultsWorker do let(:pipeline_id) { 123 } it 'does not execute service' do - expect_any_instance_of(Ci::DailyReportResultService) + expect_any_instance_of(Ci::DailyBuildGroupReportResultService) .not_to receive(:execute) expect { subject } diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100755..100644 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100755..100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |