提交 65eb4699 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@13-0-stable-ee

上级 d949826f
......@@ -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 () => {};
......@@ -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
......
......@@ -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,
},
}),
]);
......
......@@ -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() },
});
},
});
......
......@@ -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"
......
......@@ -23,3 +23,5 @@ export const EDITOR_OPTIONS = {
export const EDITOR_TYPES = {
wysiwyg: 'wysiwyg',
};
export const EDITOR_HEIGHT = '100%';
......@@ -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>
# 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
......@@ -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
......
......@@ -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)
......
# frozen_string_literal: true
module Ci
class DailyReportResult < ApplicationRecord
class DailyBuildGroupReportResult < 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?
upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
end
end
end
......@@ -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|
......
......@@ -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
......
......@@ -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'
......
# 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')
# 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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
......
# 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
# 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
......@@ -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
......
......@@ -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
......
......@@ -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:
......
......@@ -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:
......
......@@ -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'
......
# 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
......@@ -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
......
......@@ -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
......
......@@ -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)
......
# 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
# 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
......@@ -10,6 +10,9 @@ export const Editor = {
initialEditType: {
type: String,
},
height: {
type: String,
},
},
render(h) {
return h('div');
......
......@@ -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 },
[],
[],
);
},
);
});
});
/* 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}`,
);
});
});
});
......
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');
});
});
});
......@@ -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', () => {
......
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);
});
});
});
......@@ -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;
......
......@@ -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);
});
......
......@@ -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', () => {
......
export * from '../../frontend/issue_show/helpers.js';
export * from '../../frontend/issue_show/mock_data';
......@@ -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
......
......@@ -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) }
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
......@@ -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 }
......
文件模式从 100755 更改为 100644
文件模式从 100755 更改为 100644
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册